{"record":{"id":"5cdf4e32da75e33d","repo":"koala73/worldmonitor","slug":"invite-expired","errorCode":"INVITE_EXPIRED","errorMessage":"INVITE_EXPIRED","messagePattern":"INVITE_EXPIRED","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"warning","filePath":"convex/payments/businessSeats.ts","lineNumber":472,"sourceCode":"  args: { grantId: v.id(\"businessProGrants\"), token: v.string() },\n  handler: async (ctx, args) => {\n    const userId = await requireUserId(ctx);\n    const identity = await resolveUserIdentity(ctx);\n    const inviteeEmail = identity?.email?.trim().toLowerCase();\n    if (!inviteeEmail) {\n      throw new ConvexError({ kind: \"INVITEE_EMAIL_UNAVAILABLE\" });\n    }\n\n    const grant = await ctx.db.get(args.grantId);\n    if (!grant) {\n      throw new ConvexError({ kind: \"GRANT_NOT_FOUND\" });\n    }\n    if (grant.status !== \"pending\") {\n      throw new ConvexError({ kind: \"INVITE_ALREADY_USED\" });\n    }\n    const now = Date.now();\n    if (grant.expiresAt <= now) {\n      throw new ConvexError({ kind: \"INVITE_EXPIRED\" });\n    }\n    if (!(await verifyBusinessInviteToken(args.grantId, args.token))) {\n      throw new ConvexError({ kind: \"INVALID_INVITE_TOKEN\" });\n    }\n    if (grant.inviteeEmail !== inviteeEmail) {\n      throw new ConvexError({ kind: \"INVITE_EMAIL_MISMATCH\" });\n    }\n    if (!sameDomain(grant.inviteeEmail, inviteeEmail)) {\n      throw new ConvexError({ kind: \"INVITE_EMAIL_MISMATCH\" });\n    }\n    if (!isCorporateDomain(inviteeEmail)) {\n      throw new ConvexError({ kind: \"INVITEE_DOMAIN_NOT_CORPORATE\" });\n    }\n\n    const businessSub = await ctx.db\n      .query(\"subscriptions\")\n      .withIndex(\"by_dodoSubscriptionId\", (q) =>\n        q.eq(\"dodoSubscriptionId\", grant.businessSubscriptionId),","sourceCodeStart":454,"sourceCodeEnd":490,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/businessSeats.ts#L454-L490","documentation":"Each grant carries a 14-day TTL (`INVITE_TTL_MS = 14 * 24 * 60 * 60 * 1000`). `acceptBusinessInvite` rejects any grant whose `expiresAt <= now`. Expired grants no longer count against the seat cap and cannot be revived — a new grant must be issued.","triggerScenarios":"Clicking an accept link more than 14 days after the invite was sent (`grant.expiresAt <= Date.now()`).","commonSituations":"User delayed accepting; invite email buried in inbox; user switched jobs and returned to the email weeks later.","solutions":["Ask the Business owner to resend the invite, which creates a fresh grant with a new 14-day window","Owner calls `inviteSeats` again for the same email (dedup logic frees the expired slot and issues a new pending grant)"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await convex.mutation(api.payments.businessSeats.acceptBusinessInvite, { grantId, token });\n} catch (err) {\n  if (err.data?.kind === 'INVITE_EXPIRED') {\n    // prompt owner to resend the invite (fresh 14-day grant)\n  } else { throw err; }\n}","preventionTips":["Accept invites promptly within the 14-day window","Surface the invite-expiry reason clearly and offer a 'request re-invite' action"],"tags":["payments","business-seats","invites","expiry","ttl"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}