{"record":{"id":"834aae772b7c57f5","repo":"koala73/worldmonitor","slug":"owner-domain-not-corporate","errorCode":"OWNER_DOMAIN_NOT_CORPORATE","errorMessage":"OWNER_DOMAIN_NOT_CORPORATE","messagePattern":"OWNER_DOMAIN_NOT_CORPORATE","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/businessSeats.ts","lineNumber":137,"sourceCode":"export const inviteSeats = mutation({\n  args: { emails: v.array(v.string()) },\n  handler: async (ctx, args) => {\n    const userId = await requireUserId(ctx);\n    const identity = await resolveUserIdentity(ctx);\n    const ownerEmail = identity?.email?.trim();\n    if (!ownerEmail) {\n      throw new ConvexError({ kind: \"OWNER_EMAIL_UNAVAILABLE\" });\n    }\n\n    const now = Date.now();\n    const businessSub = await getCoveringBusinessSubscription(ctx, userId, now);\n    if (!businessSub) {\n      throw new ConvexError({ kind: \"OWNER_NOT_BUSINESS\" });\n    }\n\n    const ownerDomain = extractDomain(ownerEmail);\n    if (!ownerDomain || !isCorporateDomain(ownerEmail)) {\n      throw new ConvexError({ kind: \"OWNER_DOMAIN_NOT_CORPORATE\" });\n    }\n\n    // Serialize concurrent inviteSeats / removeSeat calls for this Business\n    // subscription so the cap check below cannot be bypassed by a race.\n    await touchBusinessSeatLock(ctx, businessSub.dodoSubscriptionId, now);\n\n    const normalizedOwnerEmail = ownerEmail.toLowerCase();\n    // Dedupe: the existingByEmail check below only guards against emails\n    // that already had a grant BEFORE this call — a duplicate within the\n    // SAME args.emails array would otherwise slip past it (neither\n    // occurrence is in existingGrants yet) and create two grant rows for\n    // one invitee.\n    const emails = Array.from(\n      new Set(args.emails.map((e) => e.trim().toLowerCase()).filter((e) => e.length > 0)),\n    );\n    if (emails.length === 0) {\n      throw new ConvexError({ kind: \"NO_EMAILS_PROVIDED\" });\n    }","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/businessSeats.ts#L119-L155","documentation":"Thrown by `inviteSeats` when the owner's email domain is not a 'corporate' domain per `extractDomain`/`isCorporateDomain`: the domain is missing, has no dot (bare hostname), is a free/consumer provider (gmail, outlook, etc.), or is disposable/temporary. Business seat invites require the owner to be on a real company domain. Object-typed ConvexError (`{ kind: \"OWNER_DOMAIN_NOT_CORPORATE\" }`).","triggerScenarios":"A Business owner whose email is on a free provider (e.g. `@gmail.com`), a disposable domain, or a malformed/bare-hostname address calls `inviteSeats`.","commonSituations":"Owner signed up with a personal Gmail/Outlook/Yahoo address; a disposable-email address; a misconfigured email like `user@localhost`; the domain resolver flagged the company domain as disposable via the mailchecker list.","solutions":["Have the owner switch their account email to a genuine corporate domain (with a dot, not free/disposable).","If the company domain is wrongly flagged, verify it isn't on the disposable/free list and file a correction.","Gate the invite UI on `isCorporateDomain(ownerEmail)` before letting the owner proceed."],"exampleFix":"// before\nawait inviteSeats({ emails: [...] }); // owner uses user@gmail.com\n\n// after\n// owner updates account email to user@acme.com first\nawait inviteSeats({ emails: [...] });","handlingStrategy":"validation","validationCode":"const ownerEmail = user.primaryEmailAddress!.emailAddress;\nif (!isCorporateDomain(ownerEmail)) {\n  showToast(\"Use a corporate email address to invite teammates.\");\n  return;\n}\nawait inviteSeats({ emails });","typeGuard":"function isCorporateDomain(email: string): boolean {\n  const domain = email.split(\"@\").pop() ?? \"\";\n  return domain.includes(\".\") && !FREE_PROVIDERS.has(domain.toLowerCase());\n}","tryCatchPattern":null,"preventionTips":["Encourage owners to sign up with a real company domain.","Block free/disposable provider addresses at sign-up for Business accounts."],"tags":["billing","convex","business-seats","validation","email","domain"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}