{"record":{"id":"65caa439081b49bc","repo":"koala73/worldmonitor","slug":"dodo-portal-error","errorCode":"DODO_PORTAL_ERROR","errorMessage":"DODO_PORTAL_ERROR","messagePattern":"DODO_PORTAL_ERROR","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/billing.ts","lineNumber":470,"sourceCode":"  } 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\n    // classification (browser → `extractBillingErrorKind` → tag\n    // `billing_error_kind`; the user still falls back to the generic Dodo\n    // portal), and log the underlying cause here so it survives in the\n    // Convex function logs for server-side triage. WORLDMONITOR-ST.\n    const cause = err instanceof Error ? err.message : String(err);\n    console.error(\n      `[billing] Dodo customer-portal create failed for customer ${dodoCustomerId}:`,\n      cause,\n    );\n    throw new ConvexError({ kind: \"DODO_PORTAL_ERROR\" });\n  }\n\n  return { portal_url: session.link };\n}\n\nfunction getSubscriptionStatusPriority(status: string): number {\n  switch (status) {\n    case \"active\":\n      return 0;\n    case \"on_hold\":\n      return 1;\n    case \"cancelled\":\n      return 2;\n    default:\n      return 3;\n  }\n}\n","sourceCodeStart":452,"sourceCodeEnd":488,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/billing.ts#L452-L488","documentation":"Thrown in `createCustomerPortalUrlForUser` when the Dodo SDK call `client.customers.customerPortal.create(...)` throws — either a Dodo APIError (non-2xx response) or a transport failure. The underlying cause is logged server-side (`console.error` with the customer id and message), and the call is re-thrown as an object-typed ConvexError (`{ kind: \"DODO_PORTAL_ERROR\" }`) so the client tags it for Sentry while the user falls back to the generic Dodo portal.","triggerScenarios":"Dodo returns a non-2xx for the portal-session create (rate limit, 5xx, bad customer id, account suspended), or the outbound HTTPS request to Dodo fails (timeout, DNS, network). Distinct from NO_CUSTOMER: here a customer id exists but the create call itself failed.","commonSituations":"Dodo temporary outage or 5xx; transient network blip between Convex and Dodo; customer id is stale/revoked on Dodo's side; rate-limited by Dodo during a burst of portal opens.","solutions":["Retry the portal open after a short delay (transient Dodo 5xx/network often clears).","Check the Convex function logs for the logged underlying cause (`[billing] Dodo customer-portal create failed for customer ...`).","If persistent, verify the customer id is still valid in the Dodo dashboard and that the Dodo account is in good standing.","Surface a fallback 'open Dodo portal directly' link to the user while investigating."],"exampleFix":"// before\nconst { portal_url } = await getCustomerPortalUrl({});\n\n// after (client-side retry with fallback)\ntry {\n  const { portal_url } = await getCustomerPortalUrl({});\n  location.href = portal_url;\n} catch (e) {\n  if (e.data?.kind === \"DODO_PORTAL_ERROR\") {\n    location.href = \"https://pay.dodopayments.com\"; // fallback\n  } else throw e;\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"async function openPortal() {\n  for (let attempt = 0; attempt < 3; attempt++) {\n    try {\n      const { portal_url } = await getCustomerPortalUrl({});\n      location.href = portal_url;\n      return;\n    } catch (e: any) {\n      if (e?.data?.kind === \"DODO_PORTAL_ERROR\" && attempt < 2) {\n        await new Promise(r => setTimeout(r, 500 * (attempt + 1)));\n        continue;\n      }\n      location.href = \"https://pay.dodopayments.com\"; // fallback\n      return;\n    }\n  }\n}","preventionTips":["Retry once or twice — most DODO_PORTAL_ERROR causes are transient (5xx/network).","Provide a fallback direct Dodo portal link when retries are exhausted.","Check Convex logs for the underlying cause string before escalating."],"tags":["billing","convex","dodo-payments","network","retry","customer-portal"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}