Clients (Core)
Part of the Anaya Care product wiki. See 00-overview.md.
Purpose
The Clients module is the system of record for the people receiving care ("clients"/"residents"). It owns:
- The core client profile (demographics, address, timezone, narrative, AI summary, health baselines, DNR/advance-directive flags) —
apps/backend/src/clients/entities/client.entity.ts - The client lifecycle status state machine (Draft → Active → OnHold → Closed) —
packages/shared/src/client-status-transitions.ts - Satellite documents created with every client: service information, external care providers (home-health orgs and pharmacies), and the medical record —
apps/backend/src/clients/services/clients.service.ts:211-314 - The care-team link between clients and internal CareProvider users (
CaregiverAssignment) —apps/backend/src/clients/services/client-caregiver-assignments.service.ts - Linking clients to Representative users (family/decision-makers) and MedicalProfessional users —
apps/backend/src/clients/services/clients.service.ts:1788-2159 - Cascade cleanup when a client is deleted —
apps/backend/src/clients/services/client-cleanup.service.ts - Aggregate read models: tracking-summary counts for the client sidebar (
client-tracking-summary.service.ts) and enriched report data for care-summary PDFs (report-data-enricher.service.ts)
Everything is tenant-scoped: every entity carries a required business ref, auto-injected into queries by the global Mongoose business-scope plugin (apps/backend/src/common/plugins/business-scope.plugin.ts:41-66).
Naming note: the
Familyrole was renamed toRepresentative(apps/backend/src/migrations/025-family-to-representative-rename.ts);FamilyProfilebecameRepresentativeProfile(collectionrepresentative_profiles). There is noFamilyProfileentity in the codebase today.
Entities & Data Model
Client — collection clients
apps/backend/src/clients/entities/client.entity.ts
| Field | Type | Notes |
|---|---|---|
business | ObjectId → Business | required, indexed (client.entity.ts:61-67) |
patientId | string | 6-char id from a UUID, generated server-side (clients.service.ts:465-467); no uniqueness constraint |
firstName / lastName / preferredName | string | |
image | string | avatar URL auto-generated if missing (clients.service.ts:221-225) |
dateOfBirth | Date | |
sex | enum ClientSex | |
address | Address subdoc | includes lat/lng, used for timezone + caregiver distance |
timezone | string | resolved from address via Google Maps (clients.service.ts:212-215) |
benefitsInquiry | BenefitsInquiry subdoc | |
weight / height | { value, unit } | |
havePets / petsDescription | boolean / string | |
primaryLanguage, maritalStatus | string / enum | |
careProposal | ObjectId → CareProposal | set when created from a proposal (clients.service.ts:359) |
service | ObjectId → ClientService | 1:1 satellite doc |
careProviders | ObjectId → ClientCareProviders | 1:1 satellite doc (external orgs, not caregivers) |
medicalRecord | ObjectId → ClientMedicalRecord | 1:1 satellite doc |
medicalProfessionals | [{ user: ObjectId → User }] | embedded array (client.entity.ts:143-144) |
statusHistory | [{ status, notes, timestamp, author }] | newest entry at index 0; current status = statusHistory[0].status (clients.service.ts:568-569) |
narrative | string | personal-history narrative (AI-assisted) |
aiSummary / aiSummaryUpdatedAt | string / Date | |
healthBaselines | object (HealthBaselines) | client.entity.ts:158-159 |
hasDnrOrder | boolean, default false | legacy — actively forced to false by sync (see Gaps #7) |
hasAdvanceDirective | boolean, default false | synced from Care Lock documents (dnr-sync.listener.ts:24-47) |
author | ObjectId → User | creator |
Indexes: { business: 1, createdAt: -1 } (client.entity.ts:172). toJSON normalizes _id → id (client.entity.ts:48-56) — true for all entities below as well.
ClientService — collection client_services
apps/backend/src/clients/entities/client-service.entity.ts
| Field | Type | Notes |
|---|---|---|
business | ObjectId → Business | required, indexed |
client | ObjectId → Client | required |
type | enum ClientServiceType | In-Home Care, Hospice Care, Respite Care, Placement (client-service.entity.ts:5-10); defaults to In-Home Care on manual create (clients.service.ts:259), copied from proposal otherwise (clients.service.ts:375) |
startDate | Date | required |
endDate | Date? | nullable |
ClientCareProviders — collection client_care_providers
apps/backend/src/clients/entities/client-care-providers.entity.ts. Despite the name, this is not the care team — it stores external provider organizations.
| Field | Type | Notes |
|---|---|---|
business | ObjectId → Business | required, indexed |
client | ObjectId → Client | required |
homeHealthProviders | [HomeHealthProvider] | { type, name, phone, email, fax, address } (client-care-providers.entity.ts:6-25) |
pharmacies | [Pharmacy] | { name, primaryContact, phone, email, fax, address } (client-care-providers.entity.ts:30-49) |
A lazy self-healing migration creates this doc if missing on read (clients.service.ts:1276-1305, 2254-2301).
ClientMedicalRecord — collection client_medical_records
apps/backend/src/clients/entities/client-medical-record.entity.ts
| Field | Type | Notes |
|---|---|---|
business | ObjectId → Business | required, indexed |
client | ObjectId → Client | required |
medicalEquipments | [{ name, quantity }] | |
allergies | [Allergy] | enum allergen (Pollen…Other) + severity (Mild/Moderate/Severe) (client-medical-record.entity.ts:6-22) |
hospitalizations | [Hospitalization] | facility + dates |
surgeries | [{ name, date, notes }] | |
diagnosesAndConditions | [DiagnosisAndCondition] | status: Active/Inactive/Resolved/Chronic (client-medical-record.entity.ts:24-29) |
Self-heals: findMedicalRecord creates an empty record if the ref is dangling (clients.service.ts:1361-1383).
CaregiverAssignment — collection caregiver_assignments
apps/backend/src/clients/entities/caregiver-assignment.entity.ts. The care team link: one row per (client, caregiver) pair.
| Field | Type | Notes |
|---|---|---|
business | ObjectId → Business | required, indexed |
client | ObjectId → Client | required, indexed |
caregiver | ObjectId → User (CareProvider) | required, indexed |
invitedBy | ObjectId → User | required; may be a system actor for job-posting auto-assigns |
invitedAt | Date | required |
currentStatus | enum CaregiverInviteStatus | Invited, Accepted, Declined, Inactive, Assigned (packages/shared/src/enums/domain-status.ts:197-203) |
statusHistory | [{ status, timestamp, updatedBy, notes }] | appended on each change (push to end, unlike Client which unshifts) |
notes | string? | |
priority | number | null | care-team ordering; appended at end on invite (client-caregiver-assignments.service.ts:163-177), reorderable in bulk (:530-558) |
Indexes: unique { client: 1, caregiver: 1 }, plus { caregiver, currentStatus }, { client, currentStatus }, { client, priority } (caregiver-assignment.entity.ts:99-105).
ClientCareProviders vs CaregiverAssignment: ClientCareProviders = external organizations (home health agencies, pharmacies) stored as contact-info subdocuments. CaregiverAssignment = internal CareProvider users assigned to the client's care team, with status/priority. They are unrelated despite similar names (see Gaps #5).
RepresentativeProfile — collection representative_profiles
apps/backend/src/users/entities/representative-profile.entity.ts (users module; documented here because it is the client↔family link).
| Field | Type | Notes |
|---|---|---|
user | ObjectId → User | required |
client | ObjectId → Client | required — every representative link points at exactly one client |
relationshipType | enum RelationshipType | required |
relationshipDescription / notes | string? | |
decisionAuthorities | [DecisionAuthority] | default [] |
isActive | boolean, default true | removal is a soft-delete (clients.service.ts:2094-2097) |
Indexes: { user, isActive }, { client, isActive }, and unique { user, client } partial-filtered on isActive: true (representative-profile.entity.ts:63-68). Note: no business field, so this collection is not auto-scoped by the business plugin.
How a Representative (family) user is linked to a client: an admin either creates a new Representative user + profile (clients.service.ts:1994-2036) or links an existing Representative-role user (clients.service.ts:2038-2084). A Representative can then only read clients they have an active profile for — enforced in ClientsService.findOne (clients.service.ts:1109-1124).
CareProviderProfile — collection care_provider_profiles
apps/backend/src/users/entities/care-provider-profile.entity.ts. The caregiver's résumé profile (userId, about, workExperiences, education, licenses, skills, averageRating, isAcceptingJobs). It does not link caregivers to clients — that is CaregiverAssignment's job. It is read by caregiver matching (client-caregiver-assignments.service.ts:593-664).
DnrAcknowledgment — collection dnr_acknowledgments
apps/backend/src/clients/entities/dnr-acknowledgment.entity.ts — lives in this module and is cascade-deleted with the client (client-cleanup.service.ts:82), with endpoints on the clients controller (clients.controller.ts:1418-1454). Full behavior is covered in 08-health-monitoring.md.
Workflows & State Machines
Client status state machine
States: Draft, Active, OnHold, Closed (packages/shared/src/enums/domain-status.ts:165-170). Current status is statusHistory[0].status; new entries are unshifted to position 0 with optimistic concurrency (clients.service.ts:595-625).
Who can trigger what (packages/shared/src/client-status-transitions.ts:19-54):
- SuperAdmin / Owner: full admin transition map, bypassing permission checks (
client-status-transitions.ts:47-49). - Any role holding
BusinessPermission.CLIENTS_CHANGE_STATUS(typically Admin/CareManager): same admin map (client-status-transitions.ts:50-52). - Everyone else: empty map — no manual transitions (
client-status-transitions.ts:53). - Draft → Active is deliberately absent from the manual map (
client-status-transitions.ts:20-23). It is written only byautoActivateOnFirstPublish, triggered by thecare-provider-tasks.publishedevent when the first care plan is published (clients.service.ts:652-743; emit siteclient-care-provider-tasks.service.ts:1246-1251). The transition is recorded with the sentinel noteCLIENT_AUTO_ACTIVATION_NOTE(client-status-transitions.ts:116-117) and skipped entirely if the publish event has notriggeredByUserId(clients.service.ts:728-732). - Closed is terminal — no outgoing transitions (
client-status-transitions.ts:32), with a dedicated error message (client-status-transitions.ts:84-89).
Side-effect guard: OnHold and Closed are "blocked operation" statuses (client-status-transitions.ts:103-108) — see Business Rules.
Client creation (three paths)
All three end in Draft with the three satellite docs created and back-referenced:
- Manual (web "Add Client"):
POST /clients→createClientbuilds the Client (timezone from Google Maps, avatar if no image), then sequentially creates ClientService (In-Home Care, startDate=now), ClientCareProviders (empty), ClientMedicalRecord (empty), and emits aCREATEDplatform log (clients.service.ts:211-314). Roles: any authenticated user — there is no permission decorator on this endpoint (clients.controller.ts:126-129). - From a care proposal:
POST /clients/from-care-proposal/:careProposalId→ requires the proposal to be complete (clients.service.ts:322-326), copies clientInfo, seeds the Simplified Routine Task Inventory assessment from the proposal (clients.service.ts:423-427), then best-effort marks the proposalInServicelinking back to the new client (clients.service.ts:432-446). See 03-care-proposals.md. - Duplicate:
POST /clients/:clientId/duplicate(requiresCLIENTS_MANAGE,clients.controller.ts:131-135) — copies personal info, satellite docs, all assessments, and medications (without files); freshpatientIdand Draft status (clients.service.ts:1651-1786).
Caregiver assignment lifecycle
Roles: Admin/CareManager act on the client's care team (web); CareProvider responds (mobile).
- Assign:
POST /clients/:clientId/caregiver-assignments/:caregiverId→inviteCaregiververifies client status is not OnHold/Closed (client-caregiver-assignments.service.ts:135-141), target user has CareProvider role (:144-147), and no non-Inactive assignment exists (:150-160). The assignment is created directly inAssignedstatus ("always direct assign",:168-186) with priority appended at the end. Emitscaregiver.invitation-sentnotification event and a platform log (:191-210). - Auto-assign from job postings: the
job-application.accepted-for-clientevent triggers the sameinviteCaregiverwith a note, wrapped in CLS so business scoping works outside HTTP (listeners/caregiver-auto-assign.listener.ts:25-65). ConflictException (already assigned) is swallowed. - Status change:
PUT /clients/caregiver-assignments/:assignmentId/status→updateCaregiverStatuswrites anyCaregiverInviteStatuswith no transition validation and no role/permission restriction (client-caregiver-assignments.service.ts:215-267, controllerclients.controller.ts:1341-1352). - Accept / decline (mobile caregiver):
POST /clients/:clientId/caregiver-assignments/invitation/accept|decline— only matches assignments inInvitedstatus (client-caregiver-assignments.service.ts:269-313). Since step 1 always createsAssigned, these only fire for assignments put intoInvitedsome other way (see Gaps #4). - Remove / delete:
DELETE /clients/:clientId/caregiver-assignments/:caregiverIdhard-deletes the row and emitscaregiver.removed(client-caregiver-assignments.service.ts:487-528;removeCaregiverat:315-359is the non-Inactive variant). - Reorder:
PUT /clients/:clientId/caregiver-assignments/reorder(requiresCLIENTS_MANAGE) bulk-writes priorities after verifying every assignment belongs to the client (client-caregiver-assignments.service.ts:530-558). - Matching:
GET /clients/:clientId/caregiver-assignments/availableranks unassigned CareProviders by Haversine distance from the client's address, rating, and skill match; optionally enriches with shift-conflict and daily-hour-limit checks via ShiftsService (client-caregiver-assignments.service.ts:560-723).
Representative (family) linking
Admin-side only (web): create-new (clients.service.ts:1994-2036), select-existing (must already hold Representative role, :2038-2084), update (:2108-2142), soft-remove by isActive=false (:2086-2106), send invite (:2144-2159). Email/phone uniqueness is checked platform-wide with same-business hinting (clients.service.ts:745-796). Representative users have no self-service linking flow in this module.
Client deletion (cascade)
DELETE /clients/:clientId (requires CLIENTS_ARCHIVE, clients.controller.ts:646-659) is a hard delete (clients.service.ts:1568-1649):
- Optionally deletes the linked care proposal (+ optionally its initial assessment), otherwise just clears the proposal's InService status (
:1582-1595). - Deletes the three satellite docs (
:1597-1619). - Deletes all assessments and meal assignments (
:1621-1628). ClientCleanupService.deleteAllForClientdeletes 17 collections' rows referencing the client: caregiver assignments, schedules, care-provider tasks, care plans, task submissions (+comments), shift summaries, DNR acknowledgments, engagement activities, medication logs, doctors appointments, appointment-history summaries, shift assignments, care-readiness assessments, health observations, shift engagement assignments, incident reports (client-cleanup.service.ts:73-130).- A SuperAdmin-only orphan sweep endpoint exists for pre-cascade data:
POST /clients/admin/cleanup-orphans?dryRun=false(clients.controller.ts:117-124,client-cleanup.service.ts:142-216).
Note: RepresentativeProfile rows are not in the cascade list (see Gaps #9).
Business Rules & Constraints
- Current client status is always
statusHistory[0].status, defaulting toDraftwhen history is empty (clients.service.ts:568-569). - Manual status changes must pass the shared role/permission state machine; invalid transitions throw 403 with the explanatory message (
clients.service.ts:577-593). - Status writes use optimistic concurrency on
statusHistory.0.status— a concurrent change yields 409 (clients.service.ts:595-625); auto-activation silently no-ops on the same race (clients.service.ts:674-701). - Auto-activation only runs from
Draft(idempotent across re-publishes) and never when the publish event lacks a triggering user (clients.service.ts:664-671,:728-732). OnHold/Closedclients block: new caregiver assignments (client-caregiver-assignments.service.ts:135-141), new schedules (client-schedules.service.ts:117), and new shifts (shifts.service.ts:179).Draftis allowed so care teams can be set up during onboarding (packages/shared/src/client-status-transitions.ts:97-104). The shared comment also claims care proposals are blocked, but no usage exists in the care-proposals module (see Gaps #10).Closedis terminal; re-admission requires a new client record (packages/shared/src/client-status-transitions.ts:16-17,:84-89).- One assignment per (client, caregiver) pair — unique index (
caregiver-assignment.entity.ts:102) plus an application-level conflict check on non-Inactive assignments (client-caregiver-assignments.service.ts:150-160). - Only users with role
CareProvidercan be assigned as caregivers (client-caregiver-assignments.service.ts:144-147). - CareProvider access to a client requires an
AcceptedorAssignedassignment (validateCaregiverAccess,client-caregiver-assignments.service.ts:431-452); admins bypass viahasAdminPrivileges. - Representative users can only read clients they hold an active
RepresentativeProfilefor (clients.service.ts:1109-1124) — enforced infindOneonly, not infindAll. - A user being created as a representative/medical professional must have a platform-unique email and phone; same-business duplicates get a "Select Existing" hint (
clients.service.ts:745-796). - An existing user can only be linked as representative if their role is
Representative, and double-linking an active profile is rejected (clients.service.ts:2050-2062). - Representative removal is a soft-delete (
isActive=false), preserving history (clients.service.ts:2094-2097). - Client timezone is recomputed whenever the address coordinates change on update (
clients.service.ts:497-507). - Update uses
Object.assignfrom the DTO, but the globalValidationPipe({ whitelist: true, forbidNonWhitelisted: true })strips unknown fields, sostatusHistoryetc. cannot be mass-assigned (clients.service.ts:485,apps/backend/src/main.ts:41-44). hasAdvanceDirectiveis derived data: whenever a Care Lock document in the ADVANCE_DIRECTIVE folder changes, the flag is recomputed from the document count andhasDnrOrderis forced tofalse(listeners/dnr-sync.listener.ts:24-47; intent perapps/backend/src/scripts/migrate-dnr-orders-to-advance-directives.ts:1-10).- All queries are tenant-scoped automatically via the business-scope plugin reading CLS; callers may opt out with
_bypassBusinessScope(used by the platform-wide email/phone check,clients.service.ts:750-764). - PHI access to client detail and medical record is audit-logged via
@AuditPhiAccess(clients.controller.ts:199,:228). - List view enriches each client with caregiver assignments and the next active shift (in-progress shifts ranked first, lookback window 24h) (
clients.service.ts:955-1079).
Surfaces (Web & Mobile)
Web (Next.js — admins / care managers)
- Client list —
apps/web/app/(app)/(admin)/dashboard/clients/page.tsx: paginated list callingGET /clientswith search/status/sort params and per-status counts; "Add Client" is permission-gated client-side viausePermissions(page.tsx:26-50). - Add client —
apps/web/app/(app)/(admin)/dashboard/clients/add/page.tsx. - Client workspace —
apps/web/app/(app)/(admin)/dashboard/clients/[id]/edit/*with ~40 sub-pages:personal-details,service-information,care-providers(external orgs),medical-record,medications,medical-professionals,representatives,narrative-personal-history,health-baselines,care-team(+care-team/findfor the matching/ranking UI),schedules,shifts,calendar,care-plan,care-provider-tasks,assessments/*,care-summary, tracking pages (wound-care,health-metrics,incident-reports,essential-needs,home-inventory,lock-care,change-of-conditions,doctors-appointments), and more. Sidebar badges are fed byGET /clients/:id/tracking-summary(client-tracking-summary.service.ts:58-113). - Status change dialog —
apps/web/components/clients/change-client-status-dialog.tsx: computes the allowed next statuses with the sharedgetValidClientNextStatusesfrom the user's session role + permissions (:66-69) and hides the control withoutCLIENTS_CHANGE_STATUS(:71-73). The same machine is re-validated server-side (clients.service.ts:577-593), so this is defense-in-depth, not frontend-only.
Mobile (Expo — care providers, representatives)
- Client workspace —
apps/mobile/app/(client)/[id]/*: dashboard (index.tsx),resident-profile,care-plan,daily-tasks(+ shift calendar),medications.tsx,meals,appointments,doctor-files,engagement-activities,essential-needs,health,health-observations,home-inventory,incident-reports,wound-care,lockare(document vault),calendar.tsx. The layout only gates on "fully verified" session, not role (apps/mobile/app/(client)/_layout.tsx); which client ids a caregiver can open comes from their assignments. - Assignments & invitations —
apps/mobile/lib/caregiver-assignment-api.tswraps/clients/caregiver-assignments/my-assignments,/invitations,/my-invitations/count, and accept/decline endpoints; surfaced inapps/mobile/app/(app)/(tabs)/invitations/andapps/mobile/app/(shared)/invitations/. Note the backend currently creates assignments directly asAssigned, so the accept/decline screens have no reachableInvitedrows from the standard flow (Gaps #4). - Server-side, caregiver-facing reads are limited to the caller's own rows (
getMyAssignments/getMyInvitationsreturn[]for non-CareProvider roles,client-caregiver-assignments.service.ts:397-429).
Rules enforced only in frontend code
- The web list page hides the "Add Client" button by permission, but
POST /clientshas no@RequirePermissionsdecorator (clients.controller.ts:126-129) — creation is effectively open to any authenticated user in the business. - More broadly, most
@RequirePermissionsdecorators on this controller appear not to be enforced at runtime due to guard ordering (see Gaps #1) — making the web UI'susePermissionsgating the de facto enforcement layer for several actions.
Cross-Module Dependencies
- clients → care-proposals:
forwardRefinjection ofCareProposalsService; reads proposal completeness, marksInServiceon client creation, removes/unlinks on client delete (clients.service.ts:125-126,:316-463,:1582-1595). See 03-care-proposals.md. - care-plans/tasks → clients: listens to
care-provider-tasks.published(EventEmitter2) to auto-activate Draft clients (clients.service.ts:721-743; emitter inclient-care-provider-tasks.service.ts:1246-1251). See 04-care-plans.md. - clients → shifts: injects
ShiftsService(forwardRef) for conflict/daily-hour checks in caregiver matching (client-caregiver-assignments.service.ts:54-56,:689-720); readsShiftAssignmentfor next-shift enrichment (clients.service.ts:969-1036);shifts.service.ts:179consumesisClientOperationBlocked. See 05-schedules-shifts.md. - clients → notifications: emits
caregiver.invitation-sent,caregiver.status-updated,caregiver.removedevents consumed by the notification module (client-caregiver-assignments.service.ts:191-202,:244-256,:341-350). - clients → platform-logs: emits
PLATFORM_LOG_ACTIVITY_EVENTfor create/update/status-change/delete/duplicate and every sub-resource edit (clients.service.ts:301-311,:526-543,:627-638); PHI reads audited via@AuditPhiAccess(clients.controller.ts:199). - job-postings → clients:
job-application.accepted-for-clientevent auto-assigns the hired caregiver (listeners/caregiver-auto-assign.listener.ts:25-65). - lock-care ↔ clients: file uploads create LockCare records (
clients.service.ts:2161-2202);lockcare.document.changedevents synchasAdvanceDirective(listeners/dnr-sync.listener.ts). - clients → google-maps / avatar / pdf / AI: timezone resolution (
clients.service.ts:212-215), avatar generation (:221-225), narrative generation viaAIClientNarrativeReportService(:1534-1539), AI summary service (clients.controller.ts:422-435). - clients ← assessments / medications (in-module services): client creation seeds the Simplified RTI from the proposal; duplicate copies assessments and medications (
clients.service.ts:423-427,:1745-1771). See 02-assessments.md and 07-medications.md. - tracking summary reads (read-only, parallel counts): wound-care, health-metrics, health-observations, incident-reports, essential-needs, home-inventories, lock-care, shifts, doctors-appointments, shift summaries (
client-tracking-summary.service.ts:33-56). See 08-health-monitoring.md. - report enrichment reads: incident-reports, health-observations, business, users to build care-summary report sections (
report-data-enricher.service.ts:47-89). - cascade deletes write into: shifts, health-observations, client-engagements, incident-reports, plus 13 in-module collections (
client-cleanup.service.ts:73-108). - users module: creates/updates Representative and MedicalProfessional users and sends invites via
UsersService(clients.service.ts:1834-1836,:2014,:2158).
Open Questions & Gaps
@RequirePermissionsonClientsControllerappears unenforced.PermissionsGuardis registered only as a globalAPP_GUARD(apps/backend/src/app.module.ts:292-294) and explicitly defers (returnstrue) whenreq.useris not yet populated (apps/backend/src/auth/guards/permissions.guard.ts:67-72). Global guards run before the controller-levelJwtAuthGuard(clients.controller.ts:92), andClientsControllerdoes not bindPermissionsGuardat controller level — unlike e.g.statistics.controller.ts:9andhome-inventories.controller.ts:27, which follow the documented@UseGuards(JwtAuthGuard, PermissionsGuard)pattern. Consequence:CLIENTS_CHANGE_STATUS,CLIENTS_MANAGE,CLIENTS_VIEW_ASSIGNED,CLIENTS_ARCHIVE, andSHIFTS_EXECUTEchecks on this controller are likely no-ops at the HTTP layer (the status change still re-validates permissions in the service,clients.service.ts:577-593, but delete/duplicate/list do not). Needs runtime confirmation and likely a one-line fix.- "View assigned" scoping isn't implemented in
findAll.GET /clientsrequiresCLIENTS_VIEW_ASSIGNED(clients.controller.ts:186-187) but the query never filters by the caller's assignments or representative links — any caller passing the guard sees every client in the business (clients.service.ts:858-1100). The per-caller restriction exists only infindOneand only for Representatives (clients.service.ts:1109-1124). Cannot determine from code whether CareProviders/Representatives are meant to call this list endpoint at all. updateCaregiverStatushas no state machine and no authorization. Any authenticated user can set anyCaregiverInviteStatuson any assignment in their business (client-caregiver-assignments.service.ts:215-267; routeclients.controller.ts:1341-1352). Contrast with the carefully role-gated client status machine.- The
Invitedcaregiver flow looks dead.inviteCaregiveralways creates assignments asAssigned("always direct assign",client-caregiver-assignments.service.ts:168-175), yet accept/decline endpoints only matchInvited(:273-277,:296-300) and the mobile app ships full invitation screens (apps/mobile/app/(app)/(tabs)/invitations/). Either the invite-then-accept flow was removed verbally and the consumer screens are leftovers, orInvitedrows are produced somewhere not found in this module. Needs a decision: remove the dead path or restore opt-in invitations. - Naming collision:
ClientCareProviders(external orgs/pharmacies,client_care_providers) vsCaregiverAssignment(internal care team) vs the web routecare-providersvs the roleCareProvider. Repeated source of confusion; consider renaming the entity (e.g.ClientExternalProviders). - Dead fields written on create:
createClient/createClientFromCareProposal/duplicateClientsetcaregivers: []andfamilies: [](clients.service.ts:247-249,:360-364,:1702), but neither path exists on theClientschema anymore (families removed in migration023-profile-entity-rename-and-family-cleanup.ts). Mongoose silently drops them — should be deleted from the service code. hasDnrOrderis a zombie flag. The sync listener unconditionally forces it tofalse(dnr-sync.listener.ts:40-43) per the DNR→advance-directive migration (scripts/migrate-dnr-orders-to-advance-directives.ts), yet the field remains on the schema anddnr-acknowledgment.service.ts:56-62still ORs it into "acknowledgment required" logic. Intended end-state (keep or drop the field) cannot be determined from code.- Report metric bug:
taskCompletionRateis inflated 100×.buildCareDeliveryMetricscomputestaskCompletionRatealready as a 0–100 percentage (report-data-enricher.service.ts:138-142) and then returnsMath.round(taskCompletionRate * 100)(:186) — a 50% coverage rate is reported as 5000.coveragePercent(:189-193) computes the same ratio correctly, suggestingtaskCompletionRatewas meant to be 0–1. RepresentativeProfilerows are not cascade-deleted with the client. The cleanup list (client-cleanup.service.ts:73-108) covers 17 collections but notrepresentative_profiles, leaving active profiles pointing at deleted clients (they would also dodge the orphan sweep, which only scans cascade-tracked collections). RepresentativefindOneaccess checks would simply fail, but list/UI joins may surface dangling links.- Hard-block scope mismatch with its own documentation.
CLIENT_BLOCKED_OPERATION_STATUSESis documented as blocking "new shifts, schedules, and care proposals" (client-status-transitions.ts:97-101), butisClientOperationBlockedis only consumed by schedules, shifts, and caregiver assignments — no usage in the care-proposals module. Either the comment overstates or a proposal-side guard is missing. patientIdcollisions are possible: 6 hex chars from a UUID with no uniqueness check or index (clients.service.ts:465-467), and it is searchable as an identifier (clients.service.ts:837-839). ~16.7M space; collisions become plausible at scale within a business.- Unprotected utility/migration endpoints:
POST /clients/migrate/care-providers,GET /clients/migrate/care-providers/stats,POST /clients/clear-background-jobs(clients.controller.ts:1174,:1251-1264) have no role/permission decorators beyond JWT.getRecentUpdatedClientsandfindSnippetare likewise unrestricted (clients.controller.ts:137-140,:204-207) —findSnippetuses an aggregate matching only_id; business scoping then depends entirely on the aggregate hook of the business-scope plugin. - Sequential multi-save creation is not transactional. All three creation paths save the Client, then each satellite doc, with three intermediate
client.save()calls (clients.service.ts:254-299); a crash mid-way leaves partially-wired clients. The self-healing reads (:1276-1305,:1361-1383) paper over this for two of three satellite docs, butservicehas no self-heal. deleteClientpermission is namedCLIENTS_ARCHIVEbut performs an irreversible hard delete across 20+ collections (clients.controller.ts:646-659,clients.service.ts:1568-1649). GivenClosedwas designed as the terminal audit-preserving state, whether hard delete should remain admin-reachable looks like a product decision made verbally.