Requests
Part of the Anaya Care product wiki. See 00-overview.md.
Purpose
A Request is a formal, trackable service ticket about a specific client, exchanged between a client's Representative (the family-side user role) and the care agency's management team. It is the platform's structured alternative to chat for things that need an auditable lifecycle: schedule adjustments, care plan changes, caregiver changes, billing questions, complaints, etc. (packages/shared/src/enums/domain-status.ts:672-682).
Key signals from the code:
- Every request is about exactly one Client and is created by one user (
apps/backend/src/requests/entities/request.entity.ts— requiredclientandcreatedByrefs). - A request has a direction that is auto-derived from the creator's role: a Representative creates
RepresentativeToBusiness; anyone else createsBusinessToRepresentative(apps/backend/src/requests/requests.service.ts:80-83). - The mobile representative home pitches it as "Submit or track care requests" (
apps/mobile/components/home/representative-home.tsx:198). - Management acknowledges, resolves, or cancels; the requester can only cancel their own pending request (
packages/shared/src/request-transitions.ts).
Not to be confused with Feature Requests (apps/backend/src/feature-requests/, platform-level product feedback) or account deletion requests — those are separate modules.
Entities & Data Model
Request — collection requests
Defined in apps/backend/src/requests/entities/request.entity.ts.
| Field | Type | Notes |
|---|---|---|
business | ObjectId → Business | Required, indexed. Tenant scope. |
client | ObjectId → Client | Required, indexed. The client the request is about. |
createdBy | ObjectId → User | Required, indexed. Requester. |
direction | enum RequestDirection | Required. RepresentativeToBusiness | BusinessToRepresentative (packages/shared/src/enums/domain-status.ts:696-699). |
type | enum RequestType | Required. Schedule Adjustment, Care Plan Change, Caregiver Change, Service Request, Billing Inquiry, General Inquiry, Complaint, Other. |
customType | string? | Free text; only persisted when type === Other (requests.service.ts:91). |
subject | string | Required. |
description | string | Required. |
priority | enum RequestPriority | Default Normal. Low | Normal | High | Urgent. |
status | enum RequestStatus | Default Pending, indexed. Pending | Acknowledged | Resolved | Cancelled. |
acknowledgedBy / acknowledgedAt | ObjectId → User / Date | Set on transition to Acknowledged. |
resolvedBy / resolvedAt / resolutionNotes | User ref / Date / string | Set on transition to Resolved; notes come from the transition DTO. |
cancelledBy / cancelledAt / cancellationReason | User ref / Date / string | Set on transition to Cancelled. |
commentCount | number | Default 0. Incremented on every user comment and every status change (see Business Rules). |
createdAt / updatedAt | Date | Mongoose timestamps: true. |
Compound indexes: {business, status, createdAt}, {client, status, createdAt}, {createdBy, createdAt} (request.entity.ts:113-115).
RequestComment — collection request_comments
Defined in apps/backend/src/requests/entities/request-comment.entity.ts.
| Field | Type | Notes |
|---|---|---|
request | ObjectId → Request | Required, indexed. |
author | ObjectId → User | Required. |
content | string | Required. |
isSystemMessage | boolean | Default false. true for auto-generated status-change comments. |
createdAt / updatedAt | Date | Timestamps. |
Index: {request, createdAt} (chronological thread). Note: RequestComment has no business field, so it is not covered by the global business-scope plugin (see Business Rules).
RepresentativeProfile (in apps/backend/src/users/entities/representative-profile.entity.ts) is the link table the module uses to decide which clients a Representative may raise/see requests about (requests.module.ts registers it; requests.service.ts:68-77).
Workflows & State Machines
The lifecycle is defined in packages/shared/src/request-transitions.ts and enforced server-side in RequestsService.updateRequestStatus (requests.service.ts:259-263).
Role-to-transition mapping (request-transitions.ts:34-56):
| Role | Allowed transitions |
|---|---|
SuperAdmin, Owner, Admin | Full management map: Pending ↔ Acknowledged, either → Resolved/Cancelled. Resolved and Cancelled are terminal (request-transitions.ts:8-22). |
Representative | Pending → Cancelled only — and the service additionally requires createdBy === user (request-transitions.ts:26-31; requests.service.ts:265-272). |
Custom | Role-based map is empty; permission-based API resolves REQUESTS_MANAGE → full management map, otherwise nothing (request-transitions.ts:43-49,111-117). |
| All other roles | No transitions. |
Comment threads
- Comments are a flat, chronological thread per request (
getCommentssortscreatedAt: 1,requests.service.ts:440-444). No nesting, editing, or deletion. - Every successful status change auto-creates a system comment ("Status changed from X to Y[: notes]") with
isSystemMessage: trueauthored by the acting user (requests.service.ts:293-299). - Both user comments and status changes increment
commentCount(requests.service.ts:290,378).
Notifications
RequestsService emits three events (request.created, request.status-changed, request.comment-added — requests.service.ts:104,308,393) consumed by apps/backend/src/notification/listeners/request-notification.listener.ts:
- Created: if
RepresentativeToBusiness, notify all users in the business with a role inMANAGEMENT_ROLES(Owner/Admin/SuperAdmin/System —packages/shared/src/enums/user-role.ts:42,53); ifBusinessToRepresentative, notify all active representatives of the client (request-notification.listener.ts:147-172). - Status changed / comment added: notify both sides — all management users of the business plus all active representatives of the client, minus the actor (
request-notification.listener.ts:174-204). Notification types areRequestCreated/Acknowledged/Resolved/Cancelled/CommentAdded(packages/shared/src/enums/notification-types.ts:134-140); a status change back toPendingproduces no notification (mapStatusToNotificationTypereturns null, listener line 206-220). - Notification links point at the web route
/dashboard/requests/{id}for all recipients, including mobile-first representatives (request-notification.listener.ts:59,103,136).
Each action also emits a platform log entry (REQUEST_CREATED, REQUEST_STATUS_UPDATED, REQUEST_COMMENTED — packages/shared/src/enums/domain-status.ts:1000-1002; requests.service.ts:118-130,323-336,406-416).
Business Rules & Constraints
- Permissions: all endpoints sit behind
JwtAuthGuard+PermissionsGuard(requests.controller.ts:25). Read endpoints (GET /requests,/requests/stats,/requests/:id,/requests/:id/comments) requirerequests:view; write endpoints (POST /requests,PATCH /requests/:id/status,POST /requests/:id/comments) requirerequests:manage(requests.controller.ts:28-87;packages/shared/src/enums/business-permission.ts:186-187). Owner/SuperAdmin/System bypass permission checks entirely (apps/backend/src/auth/guards/permissions.guard.ts:74-77;PERMISSION_BYPASS_ROLES,user-role.ts:254-258). - Default permission grants: Admin and Representative both get
REQUESTS_VIEWandREQUESTS_MANAGEby default (packages/shared/src/constants/default-role-permissions.ts:123-124,300-301); the CareManager migration set also includes both (:431-432). CareProvider and MedicalProfessional get neither — they have no access to this module. - Note: creating a request and commenting both require
requests:manage, not just:view— "manage" effectively means "participate" (requests.controller.ts:29,72). - Direction is server-derived, never client-supplied: Representative →
RepresentativeToBusiness, everyone else →BusinessToRepresentative(requests.service.ts:80-83). It is not inCreateRequestDto. - Representative–client link enforcement: a Representative can only create a request for, view, or comment on a request about a client they have an active
RepresentativeProfilelink to (requests.service.ts:66-78create,:229-240detail,:359-370comment,:428-438comment list). List and stats are restricted to their linked clients' requests (:146-153,451-457). - Representative can only cancel their own request (
requests.service.ts:265-272), on top of the state machine limiting them toPending → Cancelled. - State machine is enforced server-side with a role-based check; invalid transitions return 400 with a generated message (
requests.service.ts:259-263;request-transitions.ts:84-96). Note the service uses the role-basedisValidRequestTransition— forCustom-role users this map is empty, so Custom users can never change status even if they holdrequests:manage(the permission-based helpers inrequest-transitions.ts:103-148are not called anywhere in the backend). - Tenant isolation is implicit: the
Requestschema has abusinesspath, so the global business-scope Mongoose plugin injects{ business }into find/count/aggregate operations from CLS context (apps/backend/src/common/plugins/business-scope.plugin.ts:41-86; registered inapps/backend/src/app.module.ts:203-205). The service itself rarely filters by business explicitly.RequestCommenthas nobusinessfield, so the plugin does not apply; comment access is gated only by the request lookup that precedes it (which is scoped). customTypeis persisted only whentype === Other; otherwise it is silently dropped (requests.service.ts:91). No backend validation forcescustomTypeto be present whenOtheris chosen.prioritydefaults toNormalwhen omitted (requests.service.ts:94; entity default).- Search matches
subject,description, orcustomTypewith a regex-escaped case-insensitive regex (requests.service.ts:163-170;escapeRegExpfromapps/backend/src/care-proposals/utils/security.util.ts). - Pagination defaults to
page=1, limit=10, sortedcreatedAt: -1; the list response also bundles per-status counts (requests.service.ts:135-212). - Stats endpoint returns counts by status, by type, and by priority — the priority breakdown only counts open requests (
Pending/Acknowledged) (requests.service.ts:459-477). - No delete, no edit: there is no endpoint to update a request's subject/description/priority after creation, no delete endpoint, and no way to reopen a
ResolvedorCancelledrequest (controller surface is create / list / stats / detail / status / comments only).
Surfaces (Web & Mobile)
Web (apps/web) — admin dashboard
- Sidebar entry "Requests" →
/dashboard/requests, shown when the user hasrequests:view(apps/web/components/layout/app-sidebar.tsx:337-341). - List page
app/(app)/(admin)/dashboard/requests/page.tsx→RequestList(_components/request-list.tsx): status tabs with counts, filters for type/priority/search, and a direction filter that is shown only to users withrequests:manage(request-list.tsx:78-79,195) — a frontend-only rule. - Create dialog (
_components/create-request-dialog.tsx) is rendered for every viewer of the list (request-list.tsx:122); it searches the business's full client list viaGET /clients(create-request-dialog.tsx:76-81). Creation will 403 for users lackingrequests:manage— there is no frontend gating on the button itself. - Detail page
dashboard/requests/[id]/page.tsx: status action buttons computed client-side from the shared role-basedgetValidRequestNextStatuses(request.status, userRole)([id]/page.tsx:93-96);Resolve/Cancelopen a notes dialog,Acknowledgefires immediately (:98-107). Comment thread with add-comment form. The page never checkscreatedBy, so a web-using Representative would see "Cancel" on any pending request (backend rejects non-own cancels). - API client:
apps/web/lib/api/requests.ts(all 7 endpoints); React Query hooks inapps/web/hooks/use-requests.ts.
Mobile (apps/mobile) — representatives (and admins via Care Corner)
- Route group
app/(app)/(requests)/withindex.tsx(list + status filter + FAB),create.tsx,[id].tsx(detail + comments + status actions). The group is a hidden tab (href: null,app/(app)/_layout.tsx:233) reached from:- Representative home — "Submit or track care requests" card (
components/home/representative-home.tsx:59-61,198). - Admin home — "Requests" Care Corner item (
components/home/admin-home.tsx:157-160).
- Representative home — "Submit or track care requests" card (
- Create screen builds its client picker exclusively from the representative profile store (
create.tsx:34-39;components/forms/client-select-field.tsxtakesrepresentatives: RepresentativeProfile[]). The "+" FAB is shown to everyone on the list screen (index.tsx:391), but an Admin on mobile has no representative links, so the client picker is empty — mobile creation is effectively Representative-only. - Detail screen computes action buttons from the same role-based
getValidRequestNextStatuses([id].tsx:103-105), with nocreatedByownership check and no notes prompt — mobile status updates never sendnotes([id].tsx:107-109). - Hooks:
apps/mobile/hooks/requests/use-requests.tsmirrors the web API surface.
There is no CareProvider or MedicalProfessional surface for requests on either platform.
Cross-Module Dependencies
- Clients (01-clients.md):
Request.clientis a required ref; client existence is validated on create and client names are denormalized into notifications (requests.service.ts:61-64,302-306). - Users / RepresentativeProfile: Representative access control and notification fan-out are both driven by active
RepresentativeProfilelinks (requests.module.ts;request-notification.listener.ts:160-172). - Notifications module:
RequestNotificationListenerconsumes the three request events and sends unified (in-app/push) notifications (apps/backend/src/notification/listeners/request-notification.listener.ts); types registered inapps/backend/src/notification/notification-registry.ts. - Platform Logs: every create/status-change/comment emits
PLATFORM_LOG_ACTIVITY_EVENTwith entity typeREQUEST(requests.service.ts:117-130etc.; label mapping inapps/backend/src/platform-logs/platform-logs.service.ts:177). - Auth / Permissions:
PermissionsGuard+@RequirePermissionswithrequests:view/requests:manage(packages/shared/src/enums/business-permission.ts:186-187); permission group labels inpackages/shared/src/constants/permission-groups.ts:414-415. - Business scoping: global
businessScopePluginprovides tenant isolation (apps/backend/src/common/plugins/business-scope.plugin.ts). - Shared package: enums (
domain-status.ts:662-699), state machine (request-transitions.ts), response types (packages/shared/src/types/request.ts) consumed by both frontends. RequestsServiceis exported fromRequestsModulebut no other backend module imports it — the module is self-contained.- Related-but-separate: Incidents (09-incidents-and-alerts.md) handle provider-reported safety events;
Complaintis just aRequestTypehere, with no bridge between the two modules.
Open Questions & Gaps
- Representative client-scope bypass in the list endpoint. In
getRequests, the Representative scoping setsquery.client = { $in: linkedClientIds }(requests.service.ts:146-153) but the subsequentif (clientId) query.client = clientId(:155) unconditionally overwrites it. A Representative passing?clientId=<any>can list requests for any client in the business they are not linked to (the business-scope plugin still confines it to one tenant). The detail/comment endpoints re-check the link, but list rows (subject, description, status, creator) leak. statusCountsaggregation likely broken for management users. The aggregation$matchusesbusiness: user.businessIdwherebusinessIdis astring(requests.service.ts:186-190;apps/backend/src/common/interfaces/auth-user.interfaces.ts:8). Aggregations bypass Mongoose casting, and the business-scope plugin skips injecting its properly-cast match because the first stage already matches onbusiness(business-scope.plugin.ts:77-84). A string never equals an ObjectId, so the per-status tab counts likely always return empty for Owner/Admin. Same casting issue for a Representative filtering byclientId(stringclientmatch at:188). Cannot confirm without runtime, but the types strongly suggest it.- Custom roles cannot transition requests even with
requests:manage. The backend uses role-basedisValidRequestTransition(requests.service.ts:259), which returns an empty map forUserRole.Custom(request-transitions.ts:43-49). The permission-based helpers built for exactly this case (getValidRequestNextStatusesByPermissions,request-transitions.ts:119-134) are not called anywhere in backend, web, or mobile. Custom-role staff can create/comment but never acknowledge/resolve. - Frontend shows Cancel to Representatives on requests they didn't create. Both web (
[id]/page.tsx:93-96) and mobile ([id].tsx:103-105) compute buttons purely from(status, role); the "own request only" rule (requests.service.ts:265-272) is backend-only, so a Representative tapping Cancel on a co-representative's request gets a 403 after the fact. - Creating/commenting requires
requests:manage, notrequests:view(requests.controller.ts:29,72). A custom role granted only "View Requests" can read threads but cannot reply; the permission label "Acknowledge, resolve, or cancel requests" (permission-groups.ts:415) does not mention that it also gates creation and commenting. Intent cannot be determined from code. - Admins cannot effectively create requests on mobile. The mobile create screen's client picker is fed only by the user's representative profiles (
create.tsx:34-39), yet admin-home links to the request list whose "+" FAB is unconditional (index.tsx:391). An Admin reaches an empty client selector. Business→Representative requests are creatable only on web. - Notification deep links are web-only. All three notification payloads link to
/dashboard/requests/{id}(request-notification.listener.ts:59,103,136), but Representatives are a mobile-first audience; whether the mobile app maps this link to(requests)/[id]cannot be determined from this module's code. commentCountmixes user comments and system status messages (requests.service.ts:290,378), so the badge count on list cards overstates human conversation. Whether intentional cannot be determined from code.- Mobile status changes never include notes, so
resolutionNotes/cancellationReasonare only ever populated from web ([id].tsx:107-109vs[id]/page.tsx:98-121). - No reopen path.
ResolvedandCancelledare terminal for every role (request-transitions.ts:19-21); a wrongly-resolved request requires creating a new one. - No backend validation that
customTypeaccompaniesRequestType.Other—Otherrequests can be saved with no custom type, and acustomTypesent with any other type is silently discarded (requests.service.ts:91;dto/create-request.dto.ts). - No automated tests for this module. No
*.spec.tsexists underapps/backend/src/requests/andpackages/shared/src/__tests__/has no request-transitions spec (onlycare-proposal-rerequest.spec.ts), despiteREQUEST_BASE_TRANSITIONSbeing exported "for testing" (request-transitions.ts:97-99). - Status-change notifications fan out to every management user and every representative of the client (
request-notification.listener.ts:174-204) — there is no targeting of the assignee/acknowledger; noise level on large teams is a product question the code does not answer. - Terminology drift: top-level docs (
CLAUDE.md) call the family-side role "Family", but the code usesUserRole.Representative/RepresentativeProfilethroughout this module. The module is otherwise actively surfaced (web list/detail/create + mobile list/detail/create + notifications), i.e. not dormant.