-
Notifications
You must be signed in to change notification settings - Fork 0
[EDIFIKANA] Resident Invite Flow — unit-scoped invite creation and acceptance (Phase 11) #451
Description
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 addPOST /units/{unitId}/invite) to acceptrole = RESIDENT+occupantType(OWNER or TENANT). - Validate that
unit_idis 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 resolvedoccupantTypein the invites table.
Invite Acceptance
- In
UserService.acceptInvite, remove the currentInvalidRequestExceptionstub for RESIDENT and implement the actual path:- Look up the invite; verify not expired, not accepted, not deleted.
- Verify
invite.role == RESIDENTandinvite.unitId != null. - Call
OccupantDatastore.insertOccupant(userId, unitId, occupantType, ...)to create theunit_occupantsrow. - Mark
invites.accepted_at = now().
- Do NOT create a
user_organization_mappingrow for RESIDENT acceptance.
Validation
inviteUser()inUserServiceshould rejectInviteRole.RESIDENTwith a clear error directing callers to use the unit-scoped invite endpoint instead.
Frontend
- Unit Detail Screen (from [UI] Unit List + Unit Detail + Add/Edit Unit screens (Phase 3) #398/[UI] Occupant management screens within Unit Detail (Phase 5) #405): add "Invite Resident" action (Manager+ only) that opens a form with fields: email, occupant type (Owner / Tenant).
- Invite Accept Screen ([AUTH] Invitation accept flow — screens, ViewModel, deep link (Phase 1) #391): extend
InvitationAcceptScreento handle RESIDENT invites — show unit info instead of org role; on acceptance, the user is routed to the Resident navigation graph ([UI] Resident experience — role-based navigation and home screen (Phase 9) #408) rather than the org Dashboard. - Role-based routing ([UI] Resident experience — role-based navigation and home screen (Phase 9) #408): after accepting a RESIDENT invite, detect the user now has a
unit_occupantsrecord and route them toResidentHomeScreen.
Shared Models
InviteRole.RESIDENTalready exists (added in [DB] Update organization_memberships — role enum + invited_by + status fields (Phase 0) #418).- Ensure
OccupantTypeis in the shared model layer and used in invite request/response 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_idfails with a clear validation error. - A non-RESIDENT invite cannot carry a
unit_id. - On acceptance, a
unit_occupantsrow is created for the user; nouser_organization_mappingrow is created. unit_occupants.occupant_typeis 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
- [DB] Update organization_memberships — role enum + invited_by + status fields (Phase 0) #418 —
invites.unit_idcolumn androle = 'RESIDENT'CHECK constraint (DB-10b) - [DB] Create unit_occupants table (Phase 0) #382 —
unit_occupantstable schema - [BE] OccupantController + OccupantService + OccupantDatastore (Phase 0) #419 —
OccupantDatastore.insertOccupantimplementation - [AUTH] Invitation accept flow — screens, ViewModel, deep link (Phase 1) #391 — Invite accept screen (extend, not replace)
- [UI] Occupant management screens within Unit Detail (Phase 5) #405 — Unit Detail screen (add Invite Resident entry point)
- [UI] Resident experience — role-based navigation and home screen (Phase 9) #408 — Resident navigation graph (acceptance routing)
Open Questions
- Should the RESIDENT invite use the same
POST /invitesendpoint withrole = RESIDENT, or a dedicatedPOST /units/{unitId}/invite? A dedicated endpoint makes RBAC and validation cleaner. - When a RESIDENT invite is accepted and a
unit_occupantsrow is created, should the user also receive read-only access to the org (e.g., a restricteduser_organization_mappingrow), or isunit_occupantsRLS 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_occupantsrecord, 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