{"record":{"id":"015352ef981c16b1","repo":"koala73/worldmonitor","slug":"invitee-domain-not-corporate","errorCode":"INVITEE_DOMAIN_NOT_CORPORATE","errorMessage":"INVITEE_DOMAIN_NOT_CORPORATE","messagePattern":"INVITEE_DOMAIN_NOT_CORPORATE","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/businessSeats.ts","lineNumber":185,"sourceCode":"    const existingByEmail = new Map(\n      existingGrants.map((g) => [g.inviteeEmail, g]),\n    );\n\n    // Separate new invites from duplicates before the cap check so a duplicate\n    // re-invite is idempotent even when the cap is full.\n    const newEmails: string[] = [];\n    const results: Array<{\n      email: string;\n      grantId: string;\n      status: \"created\" | \"already_pending\" | \"already_accepted\";\n    }> = [];\n\n    for (const email of emails) {\n      if (email === normalizedOwnerEmail) {\n        throw new ConvexError({ kind: \"CANNOT_INVITE_SELF\" });\n      }\n      if (!isCorporateDomain(email)) {\n        throw new ConvexError({ kind: \"INVITEE_DOMAIN_NOT_CORPORATE\" });\n      }\n      if (!sameDomain(ownerEmail, email)) {\n        throw new ConvexError({ kind: \"INVITEE_DOMAIN_MISMATCH\" });\n      }\n\n      const existing = existingByEmail.get(email);\n      if (existing) {\n        if (existing.status === \"accepted\") {\n          results.push({ email, grantId: existing._id, status: \"already_accepted\" });\n          continue;\n        }\n        if (existing.status === \"pending\" && existing.expiresAt > now) {\n          results.push({ email, grantId: existing._id, status: \"already_pending\" });\n          continue;\n        }\n      }\n      newEmails.push(email);\n    }","sourceCodeStart":167,"sourceCodeEnd":203,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/businessSeats.ts#L167-L203","documentation":"Thrown by `inviteSeats` per-email when an invitee's email is not on a corporate domain (`isCorporateDomain` returns false): missing domain, no dot, a free/consumer provider, or disposable. Business seats may only go to real company addresses. Object-typed ConvexError (`{ kind: \"INVITEE_DOMAIN_NOT_CORPORATE\" }`).","triggerScenarios":"The invite list contains a personal/free address (gmail, outlook, yahoo) or a disposable/temporary email, or a malformed address with no dotted domain.","commonSituations":"Owner typed a teammate's personal Gmail instead of their work address; a throwaway email; a typo producing an invalid domain; teammate uses a consumer provider.","solutions":["Replace the invitee address with their corporate-domain email (same company as the owner).","Validate each invitee with `isCorporateDomain` client-side before submit.","Reject disposable/free addresses in the form with a clear message."],"exampleFix":"// before\nawait inviteSeats({ emails: [\"teammate@gmail.com\"] });\n\n// after\nawait inviteSeats({ emails: [\"teammate@acme.com\"] });","handlingStrategy":"validation","validationCode":"for (const e of emails) {\n  if (!isCorporateDomain(e)) {\n    showToast(`${e} is not a corporate email.`);\n    return;\n  }\n}\nawait inviteSeats({ emails });","typeGuard":"function isCorporateDomain(email: string): boolean {\n  const domain = (email.split(\"@\").pop() ?? \"\").toLowerCase();\n  return domain.includes(\".\") && !FREE_PROVIDERS.has(domain);\n}","tryCatchPattern":null,"preventionTips":["Validate each invitee with the same corporate-domain rule the server uses.","Reject free/disposable providers with a clear message."],"tags":["validation","convex","business-seats","billing","email","domain"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}