W020: Send invitation
Creates a tokenized invitation for a prospective member and emails them the accept link. Triggered from Settings, Users, Invite. Idempotent on repeat invites to the same pending email.
Steps
-
Block invites to existing members. Call
Platform.userExistsByEmailInOrg(email, orgId). If true, reject. -
Reject
ownerrole. Ownership transfer is a separate (out-of-scope-for-v1) flow. -
Short-circuit on existing pending invite. Call
Platform.findPendingInvitation(orgId, email). If a pending unexpired invite already exists for this email in this org, re-send the email and return that invite. Skip steps 4 and 5. -
Persist the invitation. Call
Platform.createInvitation({ orgId, email, role, invitedBy }). The L2 op generates the opaque token and setsexpires_at = NOW() + 7 daysby DB default. -
Send the invitation email. Subject and body include the accept link with the token. Email failure is surfaced to the caller as a warning but does not roll the invitation back; admins can resend later (W009).
Returns
The created (or pre-existing pending) invitation.
Business rules
- Owner or admin only. Authorization is enforced at the request boundary.
- Re-inviting is idempotent. A second invite for the same email in the same org returns the existing pending invite and re-sends its email. No duplicate row is created.
- Tokens are opaque, single-use, and expire. The Accept workflow (W011) rejects expired or already-consumed tokens.
- Owner role is not invitable. Owners are appointed via ownership transfer (future), not invitations.
Errors
ValidationError. The email already belongs to a member of this org, or the requested role isowner.