{"record":{"id":"a5ffa123b08986e0","repo":"koala73/worldmonitor","slug":"owner-not-business","errorCode":"OWNER_NOT_BUSINESS","errorMessage":"OWNER_NOT_BUSINESS","messagePattern":"OWNER_NOT_BUSINESS","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/businessSeats.ts","lineNumber":131,"sourceCode":"\n/**\n * Owner invites up to 4 teammates at any corporate email domain. Pending\n * invites count against the cap; each gets a single-use HMAC token emailed via Resend.\n */\nexport 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(","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/payments/businessSeats.ts#L113-L149","documentation":"Structured ConvexError thrown by inviteSeats when the signed-in owner has no covering Business subscription at the current time. Seat management is an owner privilege tied to an active Business plan; users without one cannot invite teammates.","triggerScenarios":"Thrown at convex/payments/businessSeats.ts:132 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Subscribe to (or renew) the Business plan before inviting seats","If a subscription exists, verify its status is covering and not expired/cancelled","Refresh the client after payment completes so entitlements are re-evaluated"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}