Anaya Care Docs

Care Proposals

Part of the Anaya Care product wiki. See 00-overview.md.

Purpose

A care proposal is the agency-side document that turns an intake (initial assessment) into a concrete, priced offer of care for a client: client info, a routine-task assessment, a written care plan, a service definition (type, value-added services, weekly schedule), and a rate computation. It moves through an internal review/approval workflow, is emailed to an external "representative" (the client's contact person, who gets an auto-created account), and — once the representative signs and accepts — is converted into an actual Client record, at which point the proposal enters its terminal In Service state (apps/backend/src/care-proposals/, packages/shared/src/care-proposal-transitions.ts).

Entities & Data Model

CareProposal — collection care_proposals

Defined in apps/backend/src/care-proposals/entities/care-proposal.entity.ts.

FieldTypeNotes
businessObjectId ref Business, required, indexedTenant scope; auto-set from CLS by the global business-scope plugin (apps/backend/src/common/plugins/business-scope.plugin.ts:718-725 pre-validate hook)
clientInfoembedded objectName, DOB, sex, address, weight/height, pets, contacts[] (with isPrimary), benefitsInquiry, referral source, reason for seeking help (entity lines 65-130)
assessmentObjectId ref CareProposalAssessment, nullable, default nullThe proposal's own copy of the routine-task inventory (entity lines 132-138)
carePlanembeddedplan (cover letter + addressee + careGoal + 9 IdValue[] care-domain lists), lastUpdated, isCompleted (lines 140-203)
serviceembeddedtype (In-Home Care / Hospice Care / Respite Care / Placement / null), vas (MD/CT/HA/AF/CB/CM booleans), 7-day schedules, isCompleted (lines 205-249)
rateComputationembeddedassessmentScore, hourlyRate, dailyRate, hoursRates keyed '2'..'24' + '24split', isCompleted (lines 251-295)
statusHistoryarray, newest-first{ status, notes, timestamp, author, metadata? }; current status = statusHistory[0].status — there is no separate status field (lines 297-338)
filesObjectId[] ref LockCareSigned PDFs land here on representative acceptance (lines 340-344)
reviewersObjectId[] ref UserAssigned reviewer pool, set on submit-for-approval (lines 346-350)
emailHistoryarray, newest-firstPer-send: sentAt, sentBy, includesAttachment, recipients[] each with Resend emailId and delivery statuses[] (Sent/Delivered/Bounced/Opened/Clicked/…) (lines 352-401)
authorObjectId ref User, requiredProposal owner; transferable (lines 403-408)
clientObjectId ref ClientSet when a Client is created from the proposal (line 410-411)
narrative, narrativeGeneratedAtstring/date, nullableCarried over from the initial assessment (lines 413-417)
aiSummary, aiSummaryUpdatedAtstring/dateAI-generated summary, auto-refreshed after assessment saves (lines 419-423; care-proposals.service.ts:1105-1111)

Indexes: {business, createdAt}, {emailHistory.recipients.emailId} (entity lines 430-431).

CareProposalAssessment — collection care_proposal_assessments

apps/backend/src/care-proposals/entities/care-proposal-assessment.entity.ts.

FieldTypeNotes
businessObjectId ref Business, required, indexed
assessmentSimplifiedRoutineTaskInventory subdocument, default nullThe score+notes inventory (SRTI)
careProposalObjectId ref CareProposalBack-link
lastUpdated, isCompletedDate / booleanlastUpdated auto-set and DTO-validated in a pre('save') hook (lines 58-81)

One is created and linked for every proposal at creation time — either a blank default doc (constants/initial-assessment.defaults.ts, all scores null) for directly-created proposals, or a copy of the initial assessment's SRTI for converted ones (care-proposals.service.ts:238-241, 410-414, 912-952).

CareProposalReview — collection care_proposal_reviews

apps/backend/src/care-proposals/entities/care-proposal-review.entity.ts.

FieldTypeNotes
business, careProposalObjectId refs, required, indexed
roundnumber, requiredReview round (see below)
sectionenum CareProposalReviewSectionOne reviewable section: narrative, clientInfo, assessment, coverLetter, careGoal, plus the 9 care-plan domains, etc. (packages/shared/src/enums/domain-status.ts:299+)
reviewerTypeenum CareProposalReviewerTypeRecords who reviewed (admin vs representative side)
statusenum CareProposalReviewStatusApproved or Flagged
comment, authorstring / ObjectId ref User
resolved, resolvedBy, resolvedAtboolean / ref / dateAuthor "addressed" workflow
replies[]embedded CareProposalReviewReplybody, kind (Comment | Resolution), author, versionAtReply (required ref)
versionAtFilingObjectId ref CareProposalVersion, requiredVersion snapshot the reviewer was looking at

Indexes: {careProposal, round}, {careProposal, resolved}, {business, author, resolved} (lines 141-143).

CareProposalVersion — collection care_proposal_versions

apps/backend/src/care-proposals/entities/care-proposal-version.entity.ts.

FieldTypeNotes
careProposalObjectId ref, required, indexed
versionnumberMonotonic per proposal; unique index {careProposal, version} (line 105)
snapshotMixed (CareProposalSnapshot)Frozen blob of clientInfo, assessment, carePlan, service, rateComputation (care-proposal-versions.service.ts:85-101)
statusCareProposalStatusTypeStatus that triggered the snapshot
triggerActionenumsubmitted_for_review, revision_submitted, sent_to_representative, revisions_needed, approved, representative_accepted (entity lines 15-22)
createdBy, notesref User / string

Snapshots are created (fire-and-forget) on transitions to PendingApproval, SentToRepresentative, RevisionsNeeded, Approved, RepresentativeAccepted via STATUS_TO_VERSION_TRIGGER (entity lines 28-40; care-proposals.service.ts:2543-2551). A resubmission (PendingApproval whose previous status was RevisionsNeeded) is re-tagged revision_submitted (care-proposal-versions.service.ts:56-70).

Note the directionality of the initial-assessment link: InitialAssessment.careProposal points at the proposal; CareProposal has no field referencing the initial assessment. Lookups go through initialAssessmentService.findByLinkedCareProposalId (care-proposals.service.ts:1978-1979).

Workflows & State Machines

Statuses

CareProposalStatusType (packages/shared/src/enums/domain-status.ts:284-293) — note enum values contain spaces:

Draft, Pending Approval, Approved, Sent to Representative, Revisions Needed, Cancelled, Representative Accepted, In Service.

The state machine

The single source of truth is packages/shared/src/care-proposal-transitions.ts, re-exported by the backend (apps/backend/src/care-proposals/utils/status-transitions.util.ts) and consumed directly by the web UI. There are three live transition maps, selected by permission/role, plus a legacy union:

From \ MapAdmin/approver (CARE_PROPOSALS_APPROVE; also SuperAdmin/Owner/Admin roles) — USER_FACING_ADMIN_TRANSITIONS (lines 18-50)Editor-only (CARE_PROPOSALS_EDIT) — SEQUENTIAL_TRANSITIONS (lines 55-69)Representative (CARE_PROPOSALS_RESPOND / UserRole.Representative) — REPRESENTATIVE_TRANSITIONS (lines 74-90)
DraftPendingApproval, CancelledPendingApproval
PendingApprovalDraft (withdraw), CancelledDraft
RevisionsNeededPendingApproval, CancelledPendingApproval
ApprovedCancelled
SentToRepresentativeCancelledRepresentativeAccepted, RevisionsNeeded
RepresentativeAcceptedInService
InService— (terminal)
Cancelled— (terminal)

Two statuses are deliberately not reachable by picking a status:

  • Approved / RevisionsNeeded (admin side) are only written by the review-submit flow (POST /care-proposals/:id/reviews/submit), which updateStatus trusts via a metadata.kind === 'review-submit' bypass (care-proposals.service.ts:2412-2438; care-proposal-reviews.controller.ts:149-204).
  • SentToRepresentative is only written as a side effect of POST /care-proposals/:id/send-email when the proposal is Approved and at least one recipient send succeeded (care-proposals.service.ts:2241-2259).

InService is additionally written by the server itself when a Client is created from the proposal — markInService deliberately bypasses the transition map (care-proposals.service.ts:2620-2693).

The legacy CARE_PROPOSAL_BASE_TRANSITIONS union (transitions lines 96-132) is exported "for legacy consumers" and permits looser moves (e.g. Draft→Approved, SentToRepresentative→PendingApproval); it is not used by updateStatus enforcement, which uses getEffectiveCareProposalNextStatuses (permission-driven; representatives always get the representative map) (care-proposals.service.ts:2421-2425, transitions lines 234-246).

The shared module also exposes an action API (getCareProposalActions, transitions lines 351-461) that maps current status + role/permissions to UI buttons (submit, resubmit, withdraw, start-review, send-email, start-service, accept, request-changes, cancel), so frontends never consult the raw maps.

Happy path, step by step

  1. Create (Admin/CareManager — CARE_PROPOSALS_CREATE): either directly (POST /care-proposals with clientInfo; status history seeded with Draft, blank linked assessment — care-proposals.service.ts:213-278) or by converting a completed initial assessment (POST /initial-assessments/:id/convert-to-care-proposalinitial-assessments.service.ts:744-786), which copies client info, narrative, service schedule, the SRTI assessment, and pre-computes the assessment score (care-proposals.service.ts:312-454).
  2. Author fills sections (CARE_PROPOSALS_EDIT / RATES / GENERATE): client info, assessment (auto-recomputes score + AI summary), service type & schedule, rates (calculateRates from CityRate/BaseRate tables, overrideHourlyRates), care plan (optionally AI-generated via POST :id/generate-care-proposal) (care-proposals.controller.ts:119-230, 294-327).
  3. Submit for approval (author): PATCH :id/statusPending Approval. Requires 1-20 reviewerIds, each an active same-business user holding CARE_PROPOSALS_REVIEW (dto/update-care-proposal-status.dto.ts:28-39; care-proposals.service.ts:586-627, 2491-2512). Submission also requires all required fields present (collectMissingFieldsForSubmission, utils/section-completion.util.ts:157+). A version snapshot (submitted_for_review) is created.
  4. Review (reviewers — CARE_PROPOSALS_REVIEW/CARE_PROPOSALS_APPROVE) on the web review screen: per-section reviews are filed (POST :id/reviews, bulk-approve), each stamped with versionAtFiling (care-proposal-reviews.service.ts:57-145). The reviewer then submits the round (POST :id/reviews/submit, requires CARE_PROPOSALS_APPROVE): outcome is auto-determined — any unresolved flag → Revisions Needed, otherwise Approved; at least one section must have been reviewed (care-proposal-reviews.controller.ts:154-204).
  5. Send to representative (CARE_PROPOSALS_SEND): POST :id/send-email. Requires the proposal complete; auto-creates a Representative user account (with temp password) for each recipient that matches a listed contact; emails a view link (FRONTEND_URL/view/:id?email=…) and optionally a PDF attachment; on success from Approved, auto-transitions to Sent to Representative and snapshots (care-proposals.service.ts:2039-2300).
  6. Representative accepts (Representative — CARE_PROPOSALS_RESPOND) on the web /view/:id page: signs a signature canvas and PATCH :id/statusRepresentative Accepted with the base64 signature. Backend verifies the actor's email is one of clientInfo.contacts emails (care-proposals.service.ts:2441-2459), stores a signed PDF in LockCare and pushes it to files (2517-2531), snapshots, and sends a welcome email with mobile-app download links to the primary contact (2585-2592, 2694+).
  7. Start service / create client (Admin): either the admin presses Start Service (PATCH :id/statusIn Service via the admin map) or — the richer path — presses Create Client (POST /clients/from-care-proposal/:careProposalId), which builds the Client and then calls markInService (see "Approval side-effects" below).

Rejection / revision paths

There are two distinct sources of Revisions Needed (collapsed into one status; the "who asked" lives on each review's reviewerType — version entity lines 11-14):

  • Internal review rejection: reviewer submits a round with unresolved flagged sections → Pending ApprovalRevisions Needed (review-submit flow, step 4 above). The author then resolves each flag (PATCH :id/reviews/:reviewId/resolve, requires a "what changed" reply appended as a Resolution-kind reply — care-proposal-reviews.service.ts:247-317) and re-requests review (POST :id/reviews/rerequest). Re-request gating is the shared predicate canRerequestReview (packages/shared/src/care-proposal-rerequest.ts): allowed only when status is Revisions Needed, the current round has ≥1 flag, and 0 unresolved flags; backend rejects and web disables the button from the same predicate (care-proposal-reviews.service.ts:324-390). Re-request transitions back to Pending Approval, reusing the proposal's already-assigned reviewers when none are passed (care-proposals.service.ts:2484-2512), and records {round, addressedFlagIds, kind:'rerequest'} in status-history metadata.
  • Representative requests changes: from Sent to Representative the representative picks Request ChangesRevisions Needed (representative map, transitions lines 82-86). The web view composes the notes from the representative's flagged sections (apps/web/app/(app)/(view)/view/[id]/_components/representative-review-outline.tsx:185-205). After the team revises, Send Email from the admin is the path back out (subject becomes "Revised Care Proposal …" when any Revisions Needed appears in history — care-proposals.service.ts:2062-2068, 2148-2152); per the round-precursor rules a SentToRepresentative following RevisionsNeeded counts as a new representative round.

An admin can also withdraw a submission (Pending ApprovalDraft) and later resubmit (transitions lines 26-32).

Review rounds (packages/shared/src/care-proposal-review-round.ts)

A "round" groups reviews so resolved flags from a previous cycle don't block the next. getCareProposalReviewRound derives the round number purely from statusHistory (newest-first): it counts Pending Approval entries whose predecessor was Draft/Revisions Needed (admin review cycles) and Sent to Representative entries whose predecessor was Approved/Revisions Needed (representative cycles). Bounce-backs (e.g. PendingApproval after Approved) and resends (SentToRep after SentToRep) do not start a new round. Minimum is 1. The backend wraps this in CareProposalReviewsService.getReviewRound (care-proposal-reviews.service.ts:534-538); all gating (hasUnresolvedReviews, getReviewOutcome, re-request) filters reviews by the current round.

Version snapshots & diffs (packages/shared/src/care-proposal-snapshot-diff.ts)

Snapshots exist to answer the reviewer's question "what changed since I flagged this?". Each snapshot freezes the five reviewable sections (CARE_PROPOSAL_SNAPSHOT_SECTIONS in packages/shared/src/types/care-proposal-snapshot.ts). diffCareProposalSnapshots compares two snapshots at top-level-field-per-section granularity, returning {changed, fieldsChanged?} per section. Baked-in semantics: lastUpdated/updatedAt ignored at any depth; ''/null/undefined equivalent; dates by epoch; IdValue[] arrays compared by id (reordering is not a change); other arrays positional; objects structural (diff file lines 15-18, 23-38). Exposed via GET /care-proposals/:id/versions/diff?reviewId=… (from = that review's versionAtFiling, to = latest) or ?from=&to= for arbitrary pairs (care-proposal-versions.controller.ts:27-52, care-proposal-versions.service.ts:179-253).

CRITICAL: "A care proposal cannot be created unless it originates from an initial assessment"

This rule is NOT enforced in the backend. It is at most a convention, and the product currently has a fully supported direct-creation path. Evidence:

  • POST /care-proposals (care-proposals.controller.ts:53-63, gated only by CARE_PROPOSALS_CREATE) accepts a CreateCareProposalDto containing only clientInfo (dto/create-care-proposal.dto.ts:130-135). No assessment id, no reference of any kind to an initial assessment is accepted or required.
  • CareProposalsService.create then fabricates a blank linked CareProposalAssessment from defaultInitialAssessmentDoc (all scores null) (care-proposals.service.ts:238-241, constants/initial-assessment.defaults.ts).
  • The CareProposal schema has no field pointing at an InitialAssessment; assessment is the proposal's own CareProposalAssessment copy and is nullable, default: null (entities/care-proposal.entity.ts:132-138). The link direction is reversed: InitialAssessment.careProposal is set only by the convert flow.
  • The assessment-origin path is a separate endpoint: POST /initial-assessments/:id/convert-to-care-proposal (gated by INITIAL_ASSESSMENTS_CONVERT, initial-assessments.controller.ts:120-131), whose service guards only that the assessment isn't already converted and isn't in Draft status (initial-assessments.service.ts:744-768).
  • The web UI exposes the direct path: the proposals table's empty state links to /dashboard/care-proposals/add, which renders CareProposalClientInfoForm for any holder of CARE_PROPOSALS_CREATE and POSTs /care-proposals directly (apps/web/app/(app)/(admin)/dashboard/care-proposals/_components/care-proposals-table.tsx:159-166, add/page.tsx:16-50, apps/web/components/care-proposal/care-proposal-client-info-form.tsx:158).
  • There is also a third creation path: POST /care-proposals/:id/duplicate clones an existing proposal into a fresh Draft (care-proposals.controller.ts:65-72, service lines 465-537).
  • The only place the "from an initial assessment" framing exists in code is UI copy: the permission catalog describes CARE_PROPOSALS_CREATE as "Start a new care proposal from an initial assessment" (packages/shared/src/constants/permission-groups.ts:267). The label and the enforcement disagree.

Conclusion: enforcement is absent at DTO, service, and schema level; there is no frontend-only block either — the direct add page is live. Whether the stated rule is an unimplemented intention or stale documentation cannot be determined from code. Flagged in Open Questions & Gaps #1.

Approval side-effects: what "fully approved" actually does

  • Transitioning to Approved itself only: validates no unresolved flags and that ≥1 review exists in the current round (care-proposals.service.ts:2469-2483), writes status history, creates a version snapshot (approved trigger), and emits notification + platform-log events (2553-2607). No care plan, schedule, or caregiver records are created at Approved.
  • Representative Accepted adds: signed PDF stored to LockCare and pushed to files (2517-2531), welcome email with app download links to the primary contact (2585-2592, 2694+).
  • The real materialization happens at client creation: POST /clients/from-care-proposal/:careProposalIdClientsService.createClientFromCareProposal (apps/backend/src/clients/services/clients.service.ts:316-460). It requires the proposal complete, then creates: the Client (Draft status, patientId, narrative, timezone via Google Maps), a ClientService record carrying the proposal's service.type (start date = now, no shifts), empty ClientCareProviders and ClientMedicalRecord shells, and seeds the client's routine-task inventory from the proposal assessment (clientAssessmentsService.seedFromCareProposal, line 422-426). Finally it calls careProposalsService.markInService (best-effort) which sets proposal.client and pushes In Service unless already terminal (care-proposals.service.ts:2631-2693). No schedules/shifts and no caregiver assignments are createdcaregivers: [], shifts: [] (clients.service.ts lines 360, 379). The proposal's carePlan text is not copied onto the client by this flow.
  • Note the alternate path: an admin can also PATCH :id/statusIn Service directly from Representative Accepted (admin map, transitions lines 45-47), which marks the proposal in service without creating any client. See gap #4.

Business Rules & Constraints

  • Current status is always statusHistory[0].status; transitions are applied atomically with a findOneAndUpdate preconditioned on the current head to prevent concurrent writes (care-proposals.service.ts:2440, 2514-2541 — returns 400 "modified concurrently" on mismatch).
  • Status transitions are validated against the permission/role-selected map via getEffectiveCareProposalNextStatuses; review-submit (metadata.kind === 'review-submit' from a CARE_PROPOSALS_APPROVE holder while Pending Approval) bypasses the map for Approved/Revisions Needed (care-proposals.service.ts:2412-2438).
  • Every status change except Cancelled requires the proposal to pass field validation (collectMissingFieldsForSubmission: client name/DOB/city/region, primary contact name+email+relationship, assessment complete, service type, ≥1 active hourly rate) (care-proposals.service.ts:2461-2468; utils/section-completion.util.ts:157+).
  • Submitting for approval requires 1-20 unique reviewerIds; each must be an active user of the proposal's business holding CARE_PROPOSALS_REVIEW (dto/update-care-proposal-status.dto.ts:28-39; care-proposals.service.ts:586-627). Re-request reuses existing assigned reviewers when none are supplied (2495-2506).
  • Approved is blocked while unresolved flagged reviews exist in the current round, and (when coming from PendingApproval/RevisionsNeeded) requires at least one filed review (care-proposals.service.ts:177-211, 2469-2483).
  • Reviews/replies can only be filed once a version snapshot exists; otherwise 400 (care-proposal-reviews.service.ts:517-527).
  • Reviewers can only edit/delete their own reviews (care-proposal-reviews.service.ts:224-226, 448-450); only flagged reviews can be resolved, and resolving requires a reply (268-272; dto/care-proposal-review.dto.ts). resolve has an explicit cross-tenant guard (258-266); unresolve (APPROVE-only) does not.
  • Representative actions (Representative Accepted / Revisions Needed outside review-submit) require the acting user's email to match a clientInfo.contacts email (care-proposals.service.ts:2441-2459).
  • Send Email requires CARE_PROPOSALS_SEND and a complete proposal (care-proposals.service.ts:2055-2059); from Approved it is pre-blocked if unresolved reviews exist (2154-2163); only recipients matching listed contacts get auto-created Representative accounts (2097-2123); successful sends append to emailHistory; Resend webhook statuses update per-recipient via updateEmailStatus keyed on emailId (2855+).
  • Visibility scoping (buildViewScopeFilter, care-proposals.service.ts:1924-1961): care_proposals:view:all → everything in the business; view:own → author or assigned reviewer; CARE_PROPOSALS_RESPOND → proposals where the user's email matches a contact or past email recipient. List/read routes gate at the :own floor and let the filter narrow (care-proposals.controller.ts:74-111).
  • All proposal collections carry a required business field auto-scoped by the global Mongoose business-scope plugin (query injection + pre-validate default) (apps/backend/src/common/plugins/business-scope.plugin.ts).
  • Deleting a proposal also deletes its CareProposalAssessment, deletes the linked Client document if one exists, and either unlinks or (with ?deleteLinkedInitialAssessment=true) deletes the originating initial assessment (care-proposals.service.ts:1963-2037).
  • Signed-PDF download (GET :id/get-care-proposal-pdf?signed=true) requires status in {Approved, SentToRepresentative, RepresentativeAccepted, InService} via canDownloadCareProposalPdf and a signed file present; S3 URL is SSRF-validated (care-proposals.service.ts:2357-2384; care-proposals.controller.ts:340-371; transitions lines 465-477). The unsigned PDF preview is intentionally ungated server-side — "download gating is enforced on the frontend" (care-proposals.service.ts:2338-2340).
  • Authorship can only be transferred to management-level roles, never to the current author (care-proposals.service.ts:673-700).
  • Cancel availability in the shared action API: EDIT-or-APPROVE holders, any non-terminal status (Cancelled/InService excluded) (transitions lines 309-329) — but see gap #5 for the EDIT-only mismatch.
  • Default role grants: Admin gets the full proposal suite including REVIEW/APPROVE; CareManager (migration set) gets VIEW_ALL/CREATE/EDIT/DUPLICATE/SEND but not RATES, REVIEW, APPROVE, DELETE, or TRANSFER_AUTHORSHIP; Representative gets only CARE_PROPOSALS_RESPOND (packages/shared/src/constants/default-role-permissions.ts:32-44, 260-266, 382-392).

Surfaces (Web & Mobile)

Web — admin dashboard (apps/web/app/(app)/(admin)/dashboard/care-proposals/)

  • List /dashboard/care-proposals — table/card list with status tabs and counts, filters (search, city, dates), status-history dialog, transfer-authorship and cancel dropdown items, PDF link (page.tsx, _components/care-proposals-table.tsx, care-proposal-status-tabs.tsx, StatusHistoryDialog.tsx). The "Create Care Proposal" button (empty state) links to /add and is shown only with CARE_PROPOSALS_CREATE (care-proposals-table.tsx:159-166).
  • Create /dashboard/care-proposals/add — client-info form → POST /care-proposals, then redirects into the editor's assessment step (add/page.tsx; components/care-proposal/care-proposal-client-info-form.tsx:158-171). Renders a permission-denied panel without CARE_PROPOSALS_CREATE (frontend-only gate mirroring the backend guard).
  • Editor /dashboard/care-proposals/[id]/edit/* — stepped sections: client-info, assessment, service-information, rate-computation, proposal (care plan, incl. AI generation), view-pdf (directory under [id]/edit/). Action bar renders the shared action API (_components/CareProposalActionBar.tsx), disabling submit-class intents until all sections are complete (line 106) — frontend-only mirror of the backend submission validation. Status changes go through a confirm dialog driven by action.targetStatus (CareProposalActionDialog.tsx:144-152).
  • Review /dashboard/care-proposals/[id]/review — reviewer surface with per-section outline, client-info/assessment/narrative viewers, and diff viewers (assessment-diff-viewer.tsx, section-diff-viewer.tsx, text-diff.tsx) backed by the versions diff endpoint; submitting calls POST :id/reviews/submit.
  • Read-only view /dashboard/care-proposals/[id]/view.
  • Create Client button/dropdown — shown when current status is Representative Accepted; calls POST /clients/from-care-proposal/:id (_components/CreateClientButton.tsx:36-42).
  • Statistics tab for SuperAdmin at /dashboard/statistics includes a care-proposals tab (statistics/_components/tabs/care-proposals-tab.tsx).

Web — external representative view (apps/web/app/(app)/(view)/view/[id]/)

The link emailed to representatives. Unauthenticated visitors get an email-prefilled sign-in (ViewPdfSignIn.tsx); signed-in representatives (a "mobile-only role" exempted from web onboarding — page.tsx:40-47) see the proposal PDF (client-care-proposal-pdf.tsx), an Accept form with a signature canvas posting PATCH /care-proposals/:id/status with Representative Accepted + base64 signature (accept-care-proposal-form.tsx:39-143), and a Request Changes outline that files flagged sections and posts Revisions Needed with composed notes (representative-review-outline.tsx:185-205).

Mobile (apps/mobile/)

Mobile has no care-proposal screens. Footprint is limited to:

  • Admin home dashboard shows a "Recent Care Proposals" list fed by GET /care-proposals/recent-updated, with status color chips (components/home/admin-home.tsx:45-180; components/home/dashboard-shared.tsx:150-157). Items are display-only.
  • Care-proposal push notifications deep-link to the client overview when metadata.clientId exists, otherwise navigate nowhere (components/notifications/notification-navigation.ts:63-72).

Families do not review proposals on mobile; representative acceptance is a web flow (/view/:id). (Representatives are described as mobile-onboarded roles, but no proposal UI exists in the mobile app — see gap #9.)

Cross-Module Dependencies

  • Initial Assessments (02-assessments.md): convertToCareProposalcreateFromInitialAssessment (forwardRef'd modules — care-proposals.module.ts:117, care-proposals.service.ts:134-135); proposal delete unlinks or cascades to the assessment (care-proposals.service.ts:1978-1992); assessment delete can cascade to the proposal (initial-assessments.service.ts:788-810).
  • Clients / Care Plans (04-care-plans.md): ClientsService.createClientFromCareProposal consumes the proposal, seeds the client SRTI via clientAssessmentsService.seedFromCareProposal, and calls back into markInService (clients.service.ts:316-460). Proposal delete deletes the linked client (care-proposals.service.ts:2005-2011).
  • Scheduling (05-scheduling.md): only indirectly — client creation makes a ClientService with empty shifts (clients.service.ts:373-380); proposals carry a weekly hours grid but no shift records.
  • Rates: calculateRates reads CityRate/BaseRate models from src/base-rates/ (care-proposals.service.ts:118-121, 1321+).
  • Users/Auth: representative auto-provisioning via usersService.findOrCreateRepresentativeUser on email send (care-proposals.service.ts:2110-2118); reviewer eligibility via PermissionService.hasPermission (612-623); route guards JwtAuthGuard + @RequirePermissions throughout the controllers.
  • Notifications: care-proposal.status-changed and care-proposal.authorship-transferred events handled by notification/listeners/notification.listener.ts:45-93 → in-app/push notifications.
  • Platform Logs: every create/update/status-change/review action emits PLATFORM_LOG_ACTIVITY_EVENT audit entries (e.g. care-proposals.service.ts:256-266, 2596-2607).
  • Email: Resend via EmailSenderService with React Email templates (CareProposalEmail, CareProposalRevisedEmail); webhook delivery statuses recorded per recipient (updateEmailStatus, care-proposals.service.ts:2855+).
  • PDF / Files / LockCare: PdfService.generateCareProposalPdf for previews/attachments; signatures uploaded via FilesService to S3; signed PDFs stored as LockCare documents on acceptance (care-proposals.service.ts:2310-2384, 2742+).
  • AI: CareProposalAiSummaryService (auto-summary after assessment saves) and AIClientCareProposalService (care-plan generation, generateCareProposal) from src/ai/.

Open Questions & Gaps

  1. The "must originate from an initial assessment" rule is not enforced anywhere. Backend accepts POST /care-proposals with only clientInfo; the schema has no initial-assessment reference; the web add page is live. The only trace of the rule is UI copy on the permission label (permission-groups.ts:267). Whether direct creation is intended or a gap cannot be determined from code.
  2. POST /clients/from-care-proposal/:careProposalId has no @RequirePermissions decorator (clients.controller.ts:175-184) — any authenticated business user can convert an accepted (or even merely complete) proposal into a client and force it In Service. Completeness is checked, but status is not: createClientFromCareProposal never verifies the proposal is Representative Accepted (clients.service.ts:316-323); the frontend hides the button until acceptance, which is frontend-only gating.
  3. Deleting a proposal cascades to its linked Client (care-proposals.service.ts:2005-2011) — deleting an In Service proposal hard-deletes the live client document. No status guard prevents deleting in-service proposals; the comment at lines 1971-1973 claims custom-role restrictions "enforced via permission-based guards" but the route only checks CARE_PROPOSALS_DELETE, not the status.
  4. Two divergent paths to In Service: the admin Start Service status action marks the proposal in service without creating a client, while Create Client does both. A proposal can therefore be "In Service" with client: null. Cannot determine from code which is canonical.
  5. EDIT-only users see a Cancel action that the backend will reject: getCareProposalActions.canCancel is true for CARE_PROPOSALS_EDIT holders (transitions lines 309-329), but SEQUENTIAL_TRANSITIONS contains no Cancelled target, so updateStatus 400s the attempt (care-proposals.service.ts:2421-2438). UI affordance and enforcement disagree.
  6. Version snapshots are fire-and-forget (.catch logging only — care-proposals.service.ts:2543-2551), yet filing a review hard-requires a version (care-proposal-reviews.service.ts:517-527). A failed/slow snapshot write after first submission leaves reviewers unable to file reviews; there is no retry.
  7. unresolve lacks the cross-tenant guard that resolve has (care-proposal-reviews.service.ts:392-407 vs 258-266): any CARE_PROPOSALS_APPROVE holder can un-resolve reviews on another business's proposal by id (query-level scoping by the business plugin mitigates this only when CLS business id is set; SuperAdmin context bypasses it).
  8. Withdraw requires a complete proposal: field validation runs on every non-Cancel transition, including Pending Approval → Draft (care-proposals.service.ts:2461-2468). Intent unclear — withdrawing an incomplete submission is impossible, though such a submission largely can't exist since submit validates too.
  9. Representatives are flagged as mobile-onboarded roles (isMobileOnlyRole, view page lines 40-47) but mobile has no proposal screens — acceptance only works on the web view route. The welcome email after acceptance promotes mobile app downloads (sendRepresentativeWelcomeEmail). The intended representative mobile journey cannot be determined from code.
  10. Legacy CARE_PROPOSAL_BASE_TRANSITIONS still exported (transitions lines 93-132, 195) with looser edges (Draft→Approved, SentToRep→PendingApproval); any consumer still reading it would render/permit transitions the backend rejects. No backend usage found, but the export remains.
  11. findAll status sorting is acknowledged broken-ish: sorting by 'statusHistory.0.status' "might need aggregation for better results" (care-proposals.service.ts:1730-1733), and the status-counts baseFilter rebuilds the city/search regexes without the escapeRegExp used for the main query in the city case (1760-1762) — counts and rows can diverge on regex-special characters.
  12. getAssessment (GET :id/assessment) performs no view-scope filtering beyond the CARE_PROPOSALS_VIEW_OWN floor and tenant plugin (care-proposals.controller.ts:113-117; service lines 996-1019) — a view:own user can read the assessment of any proposal in their business by id, unlike findOne which applies buildViewScopeFilter.
  13. Unsigned PDF endpoint is deliberately ungated by status ("download gating is enforced on the frontend", care-proposals.service.ts:2338-2340) — a Draft proposal's PDF is fetchable server-side despite canDownloadCareProposalPdf existing in shared code.
  14. create() / createFromInitialAssessment() rely on the CLS business-scope plugin to set business (required field). If invoked outside an HTTP request context (CLS inactive — e.g. jobs/migrations), the save would fail validation; createFromInitialAssessment inherits whatever CLS context the converting request had, not the assessment's own business. Behavior in mismatched contexts cannot be determined from code.

On this page