{"record":{"id":"4dbdeafd392e4250","repo":"koala73/worldmonitor","slug":"no-emails-provided","errorCode":"NO_EMAILS_PROVIDED","errorMessage":"NO_EMAILS_PROVIDED","messagePattern":"NO_EMAILS_PROVIDED","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/businessSeats.ts","lineNumber":154,"sourceCode":"    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    }\n    if (emails.length > MAX_SEATS) {\n      throw new ConvexError({ kind: \"TOO_MANY_EMAILS\" });\n    }\n\n    // Check existing grants for duplicates (active or pending).\n    const existingGrants = await ctx.db\n      .query(\"businessProGrants\")\n      .withIndex(\"by_businessSubscriptionId\", (q) =>\n        q.eq(\"businessSubscriptionId\", businessSub.dodoSubscriptionId),\n      )\n      .collect();\n    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.","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/businessSeats.ts#L136-L172","documentation":"Thrown by `inviteSeats` when, after trimming, lowercasing, deduplicating, and filtering empties, the `emails` array is empty. The handler normalizes the input first, so this fires only when every supplied email was blank. Object-typed ConvexError (`{ kind: \"NO_EMAILS_PROVIDED\" }`).","triggerScenarios":"Calling `inviteSeats({ emails: [] })`, or with an array of only whitespace/empty strings (e.g. `[\"\", \"  \"]`).","commonSituations":"The invite form submitted with no recipients entered; a CSV paste of blank lines; the UI forwarded an unvalidated empty list.","solutions":["Validate the emails array is non-empty before submitting the mutation.","Disable the invite button until at least one non-blank email is entered.","Filter blank entries client-side and require `length >= 1`."],"exampleFix":"// before\nawait inviteSeats({ emails: rawEmails }); // rawEmails = [\"\", \"  \"]\n\n// after\nconst emails = rawEmails.map(e => e.trim()).filter(Boolean);\nif (emails.length === 0) return;\nawait inviteSeats({ emails });","handlingStrategy":"validation","validationCode":"const emails = rawEmails.map(e => e.trim().toLowerCase()).filter(Boolean);\nif (emails.length === 0) {\n  showToast(\"Add at least one email.\");\n  return;\n}\nawait inviteSeats({ emails });","typeGuard":"function hasEmails(arr: string[]): boolean {\n  return arr.map(e => e.trim()).filter(e => e.length > 0).length > 0;\n}","tryCatchPattern":null,"preventionTips":["Disable the invite button until at least one non-blank email is entered.","Trim and filter blanks client-side before calling."],"tags":["validation","convex","business-seats","billing"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}