{"record":{"id":"e9dbf4078ecf6fb3","repo":"koala73/worldmonitor","slug":"seat-cap-reached","errorCode":"SEAT_CAP_REACHED","errorMessage":"SEAT_CAP_REACHED","messagePattern":"SEAT_CAP_REACHED","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/businessSeats.ts","lineNumber":207,"sourceCode":"        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    }\n\n    const currentCount = await countActiveOrPendingGrants(\n      ctx,\n      businessSub.dodoSubscriptionId,\n      now,\n    );\n    if (currentCount + newEmails.length > MAX_SEATS) {\n      throw new ConvexError({ kind: \"SEAT_CAP_REACHED\" });\n    }\n\n    for (const email of newEmails) {\n      const grantId = await ctx.db.insert(\"businessProGrants\", {\n        businessSubscriptionId: businessSub.dodoSubscriptionId,\n        ownerUserId: userId,\n        inviteeEmail: email,\n        domain: ownerDomain,\n        status: \"pending\",\n        createdAt: now,\n        expiresAt: now + INVITE_TTL_MS,\n      });\n\n      const token = await signBusinessInviteToken(grantId);\n      await ctx.scheduler.runAfter(\n        0,\n        internal.payments.businessSeats.sendBusinessInviteEmail,\n        { inviteeEmail: email, ownerEmail, grantId, token },","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/payments/businessSeats.ts#L189-L225","documentation":"Structured ConvexError thrown by inviteSeats when the count of currently active or pending seat grants plus the new (non-duplicate) invites would exceed MAX_SEATS (4). The check runs after the seat lock serializes concurrent calls, so the cap cannot be bypassed by racing invitations.","triggerScenarios":"Thrown at convex/payments/businessSeats.ts:211 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Remove or revoke existing seats before inviting replacements","Drop pending invites that are no longer needed to free cap space","Surface the remaining-seat count in the UI before submission"],"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"}