Anaya Care Docs

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 — required client and createdBy refs).
  • A request has a direction that is auto-derived from the creator's role: a Representative creates RepresentativeToBusiness; anyone else creates BusinessToRepresentative (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.

FieldTypeNotes
businessObjectId → BusinessRequired, indexed. Tenant scope.
clientObjectId → ClientRequired, indexed. The client the request is about.
createdByObjectId → UserRequired, indexed. Requester.
directionenum RequestDirectionRequired. RepresentativeToBusiness | BusinessToRepresentative (packages/shared/src/enums/domain-status.ts:696-699).
typeenum RequestTypeRequired. Schedule Adjustment, Care Plan Change, Caregiver Change, Service Request, Billing Inquiry, General Inquiry, Complaint, Other.
customTypestring?Free text; only persisted when type === Other (requests.service.ts:91).
subjectstringRequired.
descriptionstringRequired.
priorityenum RequestPriorityDefault Normal. Low | Normal | High | Urgent.
statusenum RequestStatusDefault Pending, indexed. Pending | Acknowledged | Resolved | Cancelled.
acknowledgedBy / acknowledgedAtObjectId → User / DateSet on transition to Acknowledged.
resolvedBy / resolvedAt / resolutionNotesUser ref / Date / stringSet on transition to Resolved; notes come from the transition DTO.
cancelledBy / cancelledAt / cancellationReasonUser ref / Date / stringSet on transition to Cancelled.
commentCountnumberDefault 0. Incremented on every user comment and every status change (see Business Rules).
createdAt / updatedAtDateMongoose 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.

FieldTypeNotes
requestObjectId → RequestRequired, indexed.
authorObjectId → UserRequired.
contentstringRequired.
isSystemMessagebooleanDefault false. true for auto-generated status-change comments.
createdAt / updatedAtDateTimestamps.

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):

RoleAllowed transitions
SuperAdmin, Owner, AdminFull management map: Pending ↔ Acknowledged, either → Resolved/Cancelled. Resolved and Cancelled are terminal (request-transitions.ts:8-22).
RepresentativePending → Cancelled only — and the service additionally requires createdBy === user (request-transitions.ts:26-31; requests.service.ts:265-272).
CustomRole-based map is empty; permission-based API resolves REQUESTS_MANAGE → full management map, otherwise nothing (request-transitions.ts:43-49,111-117).
All other rolesNo transitions.

Comment threads

  • Comments are a flat, chronological thread per request (getComments sorts createdAt: 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: true authored 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-addedrequests.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 in MANAGEMENT_ROLES (Owner/Admin/SuperAdmin/System — packages/shared/src/enums/user-role.ts:42,53); if BusinessToRepresentative, 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 are RequestCreated/Acknowledged/Resolved/Cancelled/CommentAdded (packages/shared/src/enums/notification-types.ts:134-140); a status change back to Pending produces no notification (mapStatusToNotificationType returns 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_COMMENTEDpackages/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) require requests:view; write endpoints (POST /requests, PATCH /requests/:id/status, POST /requests/:id/comments) require requests: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_VIEW and REQUESTS_MANAGE by 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 in CreateRequestDto.
  • 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 RepresentativeProfile link to (requests.service.ts:66-78 create, :229-240 detail, :359-370 comment, :428-438 comment 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 to Pending → 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-based isValidRequestTransition — for Custom-role users this map is empty, so Custom users can never change status even if they hold requests:manage (the permission-based helpers in request-transitions.ts:103-148 are not called anywhere in the backend).
  • Tenant isolation is implicit: the Request schema has a business path, 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 in apps/backend/src/app.module.ts:203-205). The service itself rarely filters by business explicitly. RequestComment has no business field, so the plugin does not apply; comment access is gated only by the request lookup that precedes it (which is scoped).
  • customType is persisted only when type === Other; otherwise it is silently dropped (requests.service.ts:91). No backend validation forces customType to be present when Other is chosen.
  • priority defaults to Normal when omitted (requests.service.ts:94; entity default).
  • Search matches subject, description, or customType with a regex-escaped case-insensitive regex (requests.service.ts:163-170; escapeRegExp from apps/backend/src/care-proposals/utils/security.util.ts).
  • Pagination defaults to page=1, limit=10, sorted createdAt: -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 Resolved or Cancelled request (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 has requests:view (apps/web/components/layout/app-sidebar.tsx:337-341).
  • List page app/(app)/(admin)/dashboard/requests/page.tsxRequestList (_components/request-list.tsx): status tabs with counts, filters for type/priority/search, and a direction filter that is shown only to users with requests: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 via GET /clients (create-request-dialog.tsx:76-81). Creation will 403 for users lacking requests: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-based getValidRequestNextStatuses(request.status, userRole) ([id]/page.tsx:93-96); Resolve/Cancel open a notes dialog, Acknowledge fires immediately (:98-107). Comment thread with add-comment form. The page never checks createdBy, 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 in apps/web/hooks/use-requests.ts.

Mobile (apps/mobile) — representatives (and admins via Care Corner)

  • Route group app/(app)/(requests)/ with index.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).
  • Create screen builds its client picker exclusively from the representative profile store (create.tsx:34-39; components/forms/client-select-field.tsx takes representatives: 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 no createdBy ownership check and no notes prompt — mobile status updates never send notes ([id].tsx:107-109).
  • Hooks: apps/mobile/hooks/requests/use-requests.ts mirrors the web API surface.

There is no CareProvider or MedicalProfessional surface for requests on either platform.

Cross-Module Dependencies

  • Clients (01-clients.md): Request.client is 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 RepresentativeProfile links (requests.module.ts; request-notification.listener.ts:160-172).
  • Notifications module: RequestNotificationListener consumes the three request events and sends unified (in-app/push) notifications (apps/backend/src/notification/listeners/request-notification.listener.ts); types registered in apps/backend/src/notification/notification-registry.ts.
  • Platform Logs: every create/status-change/comment emits PLATFORM_LOG_ACTIVITY_EVENT with entity type REQUEST (requests.service.ts:117-130 etc.; label mapping in apps/backend/src/platform-logs/platform-logs.service.ts:177).
  • Auth / Permissions: PermissionsGuard + @RequirePermissions with requests:view / requests:manage (packages/shared/src/enums/business-permission.ts:186-187); permission group labels in packages/shared/src/constants/permission-groups.ts:414-415.
  • Business scoping: global businessScopePlugin provides 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.
  • RequestsService is exported from RequestsModule but 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; Complaint is just a RequestType here, with no bridge between the two modules.

Open Questions & Gaps

  1. Representative client-scope bypass in the list endpoint. In getRequests, the Representative scoping sets query.client = { $in: linkedClientIds } (requests.service.ts:146-153) but the subsequent if (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.
  2. statusCounts aggregation likely broken for management users. The aggregation $match uses business: user.businessId where businessId is a string (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 on business (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 by clientId (string client match at :188). Cannot confirm without runtime, but the types strongly suggest it.
  3. Custom roles cannot transition requests even with requests:manage. The backend uses role-based isValidRequestTransition (requests.service.ts:259), which returns an empty map for UserRole.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.
  4. 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.
  5. Creating/commenting requires requests:manage, not requests: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.
  6. 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.
  7. 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.
  8. commentCount mixes 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.
  9. Mobile status changes never include notes, so resolutionNotes/cancellationReason are only ever populated from web ([id].tsx:107-109 vs [id]/page.tsx:98-121).
  10. No reopen path. Resolved and Cancelled are terminal for every role (request-transitions.ts:19-21); a wrongly-resolved request requires creating a new one.
  11. No backend validation that customType accompanies RequestType.OtherOther requests can be saved with no custom type, and a customType sent with any other type is silently discarded (requests.service.ts:91; dto/create-request.dto.ts).
  12. No automated tests for this module. No *.spec.ts exists under apps/backend/src/requests/ and packages/shared/src/__tests__/ has no request-transitions spec (only care-proposal-rerequest.spec.ts), despite REQUEST_BASE_TRANSITIONS being exported "for testing" (request-transitions.ts:97-99).
  13. 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.
  14. Terminology drift: top-level docs (CLAUDE.md) call the family-side role "Family", but the code uses UserRole.Representative / RepresentativeProfile throughout this module. The module is otherwise actively surfaced (web list/detail/create + mobile list/detail/create + notifications), i.e. not dormant.

On this page