{"record":{"id":"acd58bbba39957a6","repo":"koala73/worldmonitor","slug":"no-customer","errorCode":"NO_CUSTOMER","errorMessage":"NO_CUSTOMER","messagePattern":"NO_CUSTOMER","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/billing.ts","lineNumber":442,"sourceCode":" * the rawPayload and a same-user customers row still held the answer.\n */\nexport async function createCustomerPortalUrlForUser(\n  ctx: Pick<ActionCtx, \"runQuery\">,\n  userId: string,\n): Promise<{ portal_url: string }> {\n  const dodoCustomerId = await ctx.runQuery(\n    internal.payments.billing.getDodoCustomerIdForUserPortal,\n    { userId },\n  );\n\n  if (!dodoCustomerId) {\n    // User has no subscription at all, or every sub's rawPayload lacks a\n    // usable customer_id (very rare — would mean every webhook delivery\n    // for this user dropped the customer field). Throw structured so\n    // the client surfaces the existing \"contact support\" toast\n    // (object-typed `data` so `err.data.kind` survives the wire — see\n    // `api/_convex-error.js`).\n    throw new ConvexError({ kind: \"NO_CUSTOMER\" });\n  }\n\n  const client = getDodoClient();\n  let session;\n  try {\n    session = await client.customers.customerPortal.create(\n      dodoCustomerId,\n      { send_email: false },\n    );\n  } catch (err) {\n    // The Dodo REST SDK throws a plain Error (APIError on a non-2xx Dodo\n    // response, or a transport failure) when the portal-session create\n    // fails. Convex's action runtime then masks any NON-ConvexError throw\n    // as an opaque `[Request ID: X] Server Error`, dropping the real cause\n    // from the wire — the exact opacity WORLDMONITOR-R5 fought for the\n    // missing-customer path above (this was the last unwrapped throw site).\n    // Re-throw as a structured ConvexError so the client receives\n    // `err.data.kind === 'DODO_PORTAL_ERROR'` for proper Sentry","sourceCodeStart":424,"sourceCodeEnd":460,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/billing.ts#L424-L460","documentation":"Thrown in `createCustomerPortalUrlForUser` when the user has no Dodo `customer_id` — either they have no subscription at all, or every subscription's `rawPayload` lacks a usable customer field. It is an object-typed ConvexError (`{ kind: \"NO_CUSTOMER\" }`) so the client surfaces the existing 'contact support' toast via `err.data.kind`.","triggerScenarios":"A user opens the billing/portal flow (`getCustomerPortalUrl` or the edge-gateway `internalGetCustomerPortalUrl`) but has never had a subscription created through Dodo, so no customer record resolves. Also fires in the rare case where all webhook deliveries for the user dropped the `customer_id` field.","commonSituations":"Free-tier or trial user clicking 'manage subscription' before ever paying; a user whose subscription was created off-platform; corrupted/incomplete webhook payload storage that lost `customer_id`.","solutions":["Confirm the user actually has a Dodo subscription (check `subscriptions` table for their userId).","If they should have one, inspect the stored `rawPayload` for a missing `customer_id` and re-process the originating webhook.","Surface a 'contact support' / 'no active subscription' message client-side rather than retrying the portal call.","For genuinely free users, hide the portal button until a subscription exists."],"exampleFix":"// before\nconst { portal_url } = await getCustomerPortalUrl({});\n\n// after\nconst subs = await listMySubscriptions({});\nif (subs.length === 0) { showToast(\"No active subscription to manage.\"); return; }\nconst { portal_url } = await getCustomerPortalUrl({});","handlingStrategy":"validation","validationCode":"const subs = await listMySubscriptions({});\nif (subs.length === 0) {\n  showToast(\"No active subscription to manage.\");\n  return;\n}\nawait getCustomerPortalUrl({});","typeGuard":null,"tryCatchPattern":"try {\n  const { portal_url } = await getCustomerPortalUrl({});\n} catch (e: any) {\n  if (e?.data?.kind === \"NO_CUSTOMER\") {\n    showToast(\"No subscription found. Contact support if this is unexpected.\");\n    return;\n  }\n  throw e;\n}","preventionTips":["Hide the 'manage subscription' button for users with no subscription.","Treat NO_CUSTOMER as a terminal user-facing state, not a retryable error."],"tags":["billing","convex","dodo-payments","validation","customer-portal"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}