Skip to content

[EDIFIKANA] Resident Invite Flow — unit-scoped invite creation and acceptance (Phase 11) #451

@gaaliciA1990

Description

@gaaliciA1990

Summary

Implement the end-to-end resident invite flow that allows a property Manager+ to invite a user to a specific unit as a Resident (Owner or Tenant), and for that user to accept the invite — which creates a unit_occupants row rather than a user_organization_mapping row.

The DB infrastructure for this flow is being put in place by #418 (DB-10b): the invites table will have role = 'RESIDENT' and a unit_id FK column, with a CHECK constraint enforcing that unit_id is required for RESIDENT invites. This issue implements the application layer on top of that foundation.


Background & Context

Resident users — unit owners and tenants — access Edifikana to view their unit, submit maintenance requests, and access documents. Their access is scoped to their unit, not to the org at large. Because of this, they are NOT added to user_organization_mapping when they accept an invite. Instead, accepting a RESIDENT invite creates a unit_occupants row linking the user to their specific unit.

The existing org invite flow (POST /invites + POST /invites/{id}/accept) only handles org membership roles (ADMIN, MANAGER, EMPLOYEE). RESIDENT invites require a separate path that carries a unit_id and results in a different database operation on acceptance.

The distinction between OccupantType.OWNER (unit owner) and OccupantType.TENANT (renter) should be captured at invite creation time so the unit_occupants.occupant_type column is set correctly on acceptance.


Technical Scope

Backend

Invite Creation

  • Extend POST /invites (or add POST /units/{unitId}/invite) to accept role = RESIDENT + occupantType (OWNER or TENANT).
  • Validate that unit_id is provided for RESIDENT invites and that the inviting user has at least MANAGER role in the unit's organization.
  • Record invite with role = 'RESIDENT', unit_id, and the resolved occupantType in the invites table.

Invite Acceptance

  • In UserService.acceptInvite, remove the current InvalidRequestException stub for RESIDENT and implement the actual path:
    1. Look up the invite; verify not expired, not accepted, not deleted.
    2. Verify invite.role == RESIDENT and invite.unitId != null.
    3. Call OccupantDatastore.insertOccupant(userId, unitId, occupantType, ...) to create the unit_occupants row.
    4. Mark invites.accepted_at = now().
  • Do NOT create a user_organization_mapping row for RESIDENT acceptance.

Validation

  • inviteUser() in UserService should reject InviteRole.RESIDENT with a clear error directing callers to use the unit-scoped invite endpoint instead.

Frontend

Shared Models


Acceptance Criteria

  • A Manager+ can send a RESIDENT invite for a specific unit specifying occupant type (Owner or Tenant).
  • Attempting to send a RESIDENT invite without a unit_id fails with a clear validation error.
  • A non-RESIDENT invite cannot carry a unit_id.
  • On acceptance, a unit_occupants row is created for the user; no user_organization_mapping row is created.
  • unit_occupants.occupant_type is set to the value specified at invite creation (OWNER or TENANT).
  • After acceptance, the resident is routed to the Resident navigation graph.
  • An EMPLOYEE/MANAGER invite cannot be accepted via the resident path (and vice versa).
  • Expired, already-accepted, and cancelled invites are rejected with appropriate errors.

Dependencies


Open Questions

  • Should the RESIDENT invite use the same POST /invites endpoint with role = RESIDENT, or a dedicated POST /units/{unitId}/invite? A dedicated endpoint makes RBAC and validation cleaner.
  • When a RESIDENT invite is accepted and a unit_occupants row is created, should the user also receive read-only access to the org (e.g., a restricted user_organization_mapping row), or is unit_occupants RLS sufficient to gate all their data access?
  • If a user is a TENANT in one unit and later becomes the OWNER, is that modeled as updating the existing unit_occupants record, removing and re-inviting, or creating a second row?
  • Should the invite email for RESIDENT invites show unit-specific information (unit number, property name) rather than org-level info?

Phase

Phase 11 — Post-MVP

Relates to

#418, #382, #391, #405, #408, #419

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions