/** * Structured-error introspection for `payments/billing:getCustomerPortalUrl`. * * Mirrors the edge-runtime `extractConvexErrorKind` helper in * `api/_convex-error.js`, scoped to the data shapes this Convex action * produces: `{ kind: 'NO_CUSTOMER' | 'DODO_API_KEY_MISSING' | * 'USER_ID_REQUIRED' | 'DODO_PORTAL_ERROR' }`. * * Zero deps so it can be unit-tested from `node --test` without pulling * the rest of `src/services/billing.ts`' browser-only import graph * (Sentry SDK, Convex client) into the test runner. */ /** * Extract the structured `kind` field from a thrown Convex object-data * ConvexError, falling back to a legacy substring match on the message * during the deploy-ordering window where the browser bundle and the * Convex action may briefly disagree on the throw shape. * * Returns null when neither path matches — letting the caller default * unknown shapes to error-level Sentry capture. */ export function extractBillingErrorKind(err: unknown): string | null { const data = (err as { data?: unknown } | null | undefined)?.data; if (data && typeof data === 'object' && 'kind' in data) { const kind = (data as Record).kind; if (typeof kind === 'string') return kind; } // Legacy substring fallback for the deploy-ordering window where the // browser bundle ships ahead of the Convex action update (or vice-versa). // Pre-fix, the action threw `new Error('No Dodo customer found for this // user')`; if a stale Convex deployment is still serving that text we // can still classify the event correctly. Removable once both layers // have soaked on the structured-throw rollout. const msg = err instanceof Error ? err.message : ''; if (/No Dodo customer found for this user/.test(msg)) return 'NO_CUSTOMER'; return null; }