Medications
Part of the Anaya Care product wiki. See 00-overview.md.
Purpose
The Medications module maintains a per-client medication library (prescriptions, OTC drugs, topicals, supplements), schedules doses with iCalendar RRULEs, groups medications into shared "med passes" (task groups), and turns those schedules into virtual MEDICATION_REMINDER tasks that caregivers see during shifts. When a caregiver completes a medication task with a per-medication checklist (Taken / Skipped / Unavailable), the system writes immutable MedicationLog entries, decrements stock for taken doses, and sends low-stock notifications. Medication data is enriched from public drug databases: name/dosage autocomplete via RxTerms and OpenFDA, and a caregiver-facing "guide" extracted from FDA drug-label text.
Note on terminology: "label" in this module means the OpenFDA structured drug-label document fetched by drug name (apps/backend/src/clients/services/medication-label.service.ts). There is no camera/photo label-scanning feature anywhere in the codebase (verified: no scan/camera references in web or mobile medication components).
Entities & Data Model
ClientMedication
Collection: client_medications (apps/backend/src/clients/entities/client-medication.entity.ts:32-44)
| Field | Type | Notes |
|---|---|---|
business | ObjectId → Business | required, indexed (line 48-54) |
client | ObjectId → Client | required (line 56-57) |
type | enum MedicationType | Prescription (default) | OTC | Topical | Supplement (lines 6-11, 59-63) |
name | string | required (line 65-66) |
dosageOptions | string[] | strength/form options from drug search, default [] (line 68-69) |
dosage | string | the chosen dosage (line 71-72) |
frequency | string | free text (line 74-75) |
purpose | string | free text (line 77-78) |
instructions | enum MedicationInstructions | 16 preset values incl. Custom (lines 13-30, 80-81) |
customInstructions | string | used when instructions = Custom (line 83-84) |
guide | Mixed | FDA drug-label guide (MedicationGuide shape), stored as schemaless object (line 86-87) |
files | ObjectId[] → LockCare | attachments (e.g. prescription images), default [] (lines 89-93) |
stock | number | remaining dose count (line 95-96) |
refillReminder | boolean | enables low-stock notifications (line 98-99) |
refillReminderThreshold | number | notify when stock <= threshold (line 101-102) |
isActive | boolean | default true; acts as the discontinue flag (line 104-105) |
taskGroupId | string (UUID) | med-pass group id, indexed (line 107-108) |
taskGroupName | string | display name of the group (line 110-111) |
schedules[] | embedded | { id (UUID), dtstart (Date), rrule (string), tzid (string), exdate (Date[]), label? } (lines 113-133) |
Indexes: {business, createdAt}, {client, isActive}, {client, taskGroupId} (lines 138-140).
MedicationLog
Collection: client_medication_logs (apps/backend/src/clients/entities/medication-log.entity.ts:6-18). One row per medication per dose occurrence. Append-only — no update/delete code paths exist (only inserts in the stock listener and bulk delete in client cleanup).
| Field | Type | Notes |
|---|---|---|
business | ObjectId → Business | required, indexed (lines 22-28) |
client | ObjectId → Client | required (lines 30-35) |
medication | ObjectId → ClientMedication | required (lines 37-42) |
medicationName | string | denormalized snapshot, required (lines 44-45) |
taskGroupId / taskGroupName | string | group at time of dose (lines 47-51) |
scheduleId | string | which schedule entry produced the dose (lines 53-54) |
taskId | string | the virtual task id (medgrp_...), required (lines 56-57) |
shiftAssignment | ObjectId → ShiftAssignment | (lines 59-63) |
taskSubmission | ObjectId → TaskSubmission | required (lines 65-70) |
caregiver | ObjectId → User | required (lines 72-77) |
status | enum MedicationLogStatus | TAKEN | UNAVAILABLE | SKIPPED, required (lines 79-84; enum in packages/shared/src/enums/medication-log.ts:1-5) |
reason | string | skip/unavailable reason code (line 86-87) |
notes | string | caregiver free text (line 89-90) |
occurredAt | Date | required (line 92-93) |
Indexes: {client, occurredAt}, {client, medication, occurredAt}, {shiftAssignment}, {business, createdAt}, {taskSubmission} (lines 99-103).
Supporting shared types
MedicationChecklistItem—{ medicationId, status, reason?, notes? }submitted by caregivers (packages/shared/src/types/care-provider-tasks.ts:831-836).MedicationGuide/MedicationGuideSection— FDA guide shape;FDA_LABEL_SECTION_MAPmaps 9 OpenFDA fields (e.g.boxed_warning→ "Serious Warning") to caregiver-friendly section labels (packages/shared/src/types/medication-guide.ts).MedicationUnavailableReason(OUT_OF_STOCK,PHARMACY_DELAY,DISCONTINUED,OTHER) andMedicationSkipReason(CLIENT_REFUSED,SIDE_EFFECTS,DOCTOR_ADVISED,OTHER) (packages/shared/src/enums/medication-log.ts:7-19).
There is no ClientMedicationsModule in practice — apps/backend/src/client-medications/client-medications.module.ts is an empty stub (@Module({})). All medication services, entities, and routes live in the clients module (apps/backend/src/clients/).
Workflows & State Machines
Medication creation (web, manual entry with drug-database assist)
- Admin/CareManager opens Add Medication on the client's Medications page (
apps/web/.../edit/medications/page.tsx:364-370, dialog →add-medication-form.tsx). - Typing a name triggers debounced autocomplete via
GET /clients/search-medications(apps/backend/src/clients/controllers/clients.controller.ts:192-195). The backend tries RxTerms first (returns names +STRENGTHS_AND_FORMSasdosageOptions), then falls back to OpenFDA NDC brand-name search for queries ≥ 3 chars; queries < 2 chars return nothing (apps/backend/src/clients/services/medication-search.service.ts:24-50). Selecting a result fillsdosageOptions(add-medication-form.tsx:255). - The form collects type, dosage, frequency, purpose, instructions (preset or custom), stock, refill reminder + threshold, file attachments, optional group, and schedule entries. Schedules are serialized client-side into
{ id, dtstart: ISO, rrule, tzid: clientTimezone, exdate }(add-medication-form.tsx:167-172). POST /clients/:clientId/medications(clients.controller.ts:242-249; controller overwritesdto.clientwith the path param).ClientMedicationsService.createMedicationthen (apps/backend/src/clients/services/client-medications.service.ts:415-479):- Verifies the client exists.
- If no
guidewas provided, auto-fetches the FDA drug label by name (brand name first, generic fallback; parenthetical suffixes like "(Oral Pill)" stripped) and stores the extracted sections asguide— failures are logged and swallowed (lines 424-436;medication-label.service.ts:22-61). - Resolves the task group (see below) and validates group schedule consistency.
- Saves, then propagates its schedule to all group siblings if it has one (lines 450-457).
- Creates LockCare entries for attachments with
metadata.folder = MEDICATION(lines 459-466, 860-949).
- The guide can be (re)fetched on demand via
POST /clients/:clientId/medications/:medicationId/fetch-guide, which overwrites the stored guide (client-medications.service.ts:773-806). Renaming a medication on update also re-fetches the guide (lines 523-537).
Task-group (med pass) resolution
- Create (
client-medications.service.ts:217-252): if ataskGroupNameis given without an id, the service finds an existing group on the same client by case-insensitive exact name match and joins it; otherwise it generates a new UUID. Name defaults to the medication name. Before saving,validateGroupScheduleConsistencyrejects with 400"All medications in the same group must share the same schedule."if siblings have a different schedule — unless all siblings have empty schedules, in which case the new schedule will be propagated to them (lines 140-177). - Update (
resolveTaskGroupForUpdate, lines 254-350): renaming the group without an explicit id joins an existing same-named group or mints a new UUID; explicitly setting ataskGroupIdaligns the name with that group's existing name. Schedule-consistency validation is intentionally skipped on update — the updated schedule is force-propagated to all group members after save (comment at line 348, propagation at lines 635-643). - Remove from group (web): sets a fresh random
taskGroupIdandtaskGroupName = medication.name(apps/web/.../medications/page.tsx:153-173). - Frontend-only "smart schedule lock": the edit form locks schedule editing only when other group members already have schedules (
edit-medication-form.tsx:253-264).
Virtual medication tasks (medgrp_ ids)
Medication tasks are not stored in care plans. They are computed at read time when a caregiver's shift tasks are fetched (apps/backend/src/clients/services/client-care-provider-tasks.service.ts:843-861):
- Active medications with at least one schedule (
isActive: true,'schedules.0': {$exists: true}) are loaded and passed tocomputeMedicationTasks(apps/backend/src/common/utils/compute-medication-tasks.util.ts:73-182). - Each schedule's RRULE is resolved against the shift window; medications sharing a
taskGroupIdwhose occurrences land at the same instant are merged into one group task with idmedgrp_<taskGroupId>_<scheduleId>(lines 18-23, 108-144). - The computed task has
templateType: 'MEDICATION_REMINDER',taskMode: SCHEDULED,isRequired: true,priority: 1, per-medication instruction steps, and the full medication list inmetadata.medicationGroup(lines 153-175). The task template is the single storedTaskTemplateof typeMEDICATION_REMINDER(cached;client-care-provider-tasks.service.ts:121-132). - Legacy
MEDICATION_REMINDERtasks still stored in care-plan documents are filtered out of shift task lists (client-care-provider-tasks.service.ts:827-833), and the AI care-plan generator no longer produces them (apps/backend/src/ai/agents/care-provider-tasks/care-provider-tasks-orchestrator.service.ts:360-361). - Task submission resolution falls back to parsing
medgrp_ids when the task isn't found in the care plan (apps/backend/src/clients/services/client-task-submissions.service.ts:122-150).
Administration logging during shifts
- On mobile, an in-progress
MEDICATION_REMINDERtask rendersMedicationChecklist; the caregiver must set a status for every medication in the group before the checklist is "complete" (apps/mobile/components/care-provider-tasks/medication-checklist.tsx:80-104), choosing Taken / Unavailable (reason: out of stock, pharmacy delay, discontinued, other) / Skipped (reason: client refused, side effects, doctor advised, other) plus notes (lines 31-43). The checklist items ride on the task completion call (task-details-bottom-sheet.tsx:378-391; DTOsCompleteTaskSubmissionDto.medicationChecklist/SubmitTrackingTaskDto.medicationChecklist,apps/backend/src/clients/dto/create-task-submission.dto.ts:38-52, 89-94, 120-125). emitMedicationTaskCompletedIfNeededfires only whentemplateType === 'MEDICATION_REMINDER', ashiftAssignmentIdandbusinessIdexist, and the checklist is non-empty (client-task-submissions.service.ts:1368-1396). It is called from both the tracking-task submit path and the start→complete path (lines 375-383, 755-767).- The listener (
apps/backend/src/notification/listeners/medication-stock-notification.listener.ts:50-139):- Parses the
medgrp_task id; non-medication tasks are skipped. - Loads active medications in the group; any group medication missing from the checklist is logged with default status
TAKEN(line 94). - Inserts one
MedicationLogper group medication withoccurredAt = now(not the scheduled time). - Decrements stock by 1 only for
TAKENmeds withstock > 0 && isActive(lines 114-124). - Sends
MedicationStockLownotifications whenrefillReminderis on, a threshold is set, and the newstock <= threshold— recipients are all activeMANAGEMENT_ROLESusers in the business plus the client's active representatives, linked to/dashboard/clients/:id/medications(lines 166-232).
- Parses the
Dose status (per checklist item)
MedicationLogStatus is a one-shot terminal status per dose occurrence — there are no transitions after logging:
(Mobile allows toggling a selection off before submission — medication-checklist.tsx:106-115 — but once submitted the log is immutable.)
Refill / discontinue
There is no dedicated refill or discontinue workflow. As built:
- Refill = an admin manually editing
stockvia the medication update endpoint (web edit form field,medication.schema.ts:32). Stock only decreases automatically via the listener above. - Discontinue = toggling
isActiveto false (update DTO field). Inactive medications are excluded from virtual task computation (client-care-provider-tasks.service.ts:846-849) and from the listener's logging/decrement (isActive: truefilter). "Past" tab on mobile andisActivefilter on the API expose them read-only. - Hard delete =
DELETE /clients/:clientId/medications/:medicationId, which also deletes all attached files from LockCare/S3 (client-medications.service.ts:673-726). ExistingMedicationLogrows survive deletion (they keepmedicationNamedenormalized).
Business Rules & Constraints
- All medications in a group must share one schedule. Enforced at create with a 400 error; on update the edited schedule is instead force-propagated to all siblings (
client-medications.service.ts:140-177, 348, 635-643). - Group joining is by case-insensitive exact name match within the same client, with regex-escaped name (
client-medications.service.ts:200-215). - Missing checklist entries default to TAKEN when a medication group task is completed (
medication-stock-notification.listener.ts:94). - Empty checklist = nothing happens: no logs, no stock decrement, no notifications (
client-task-submissions.service.ts:1381-1382). - Stock never goes negative: decrement uses
{ stock: { $gt: 0 } }guard (medication-stock-notification.listener.ts:118). - Low-stock alerts go to management roles + client representatives only when
refillReminderis true andrefillReminderThresholdis defined andstock <= threshold(medication-stock-notification.listener.ts:170-174). - FDA guide auto-fetch happens on create (when no guide supplied) and on rename; failures are non-fatal (
client-medications.service.ts:424-436, 523-537). The on-demand fetch endpoint 404s if no FDA label exists (client-medications.service.ts:792-796). - Drug search: minimum 2 characters; RxTerms first, OpenFDA fallback only for ≥ 3 characters; OpenFDA results deduplicated by brand name (
medication-search.service.ts:24-50, 105-124). - Tenancy is enforced by the global Mongoose business-scope plugin, which injects
{ business }into every query on schemas that have abusinesspath (both medication entities do) (apps/backend/src/common/plugins/business-scope.plugin.ts:41-66; registered viaBusinessScopeInterceptorinapp.module.ts:287-305). The medication endpoints themselves apply no role or permission decorators — only the controller-levelJwtAuthGuard(clients.controller.ts:91-92and endpoint bodies at 233-314). - PHI audit: listing medications is audited via
@AuditPhiAccess(PlatformLogEntityType.MEDICATION)(clients.controller.ts:233-235). Single-medication GET, create, update, delete, and log reads are not audited. - Platform activity logs are emitted for medication update and delete (and touch the client's
updatedAt), but not for create (client-medications.service.ts:53-75, 663-668, 718-723). - File attachments are LockCare documents owned by the client with
metadata.folder = MEDICATION; removing files on update or deleting the medication deletes them from LockCare and S3 (client-medications.service.ts:554-631, 687-708). - Reports disclaimer: period reports state caregivers provide "medication reminders only — not administration" (
apps/backend/src/clients/services/report-data-enricher.service.ts:30-31). - Frontend-only validation (not enforced by the backend DTOs): name ≥ 2 chars,
stock ≥ 0,refillReminderThreshold ≥ 1(apps/web/.../medication.schema.ts:21-41— backend only checks types,client-medication.dto.ts). - Frontend-only display rule: groups with a single member are rendered as standalone medications (
apps/web/.../medications/page.tsx:60-64).
Surfaces (Web & Mobile)
Web — admins / care managers
/dashboard/clients/[id]/edit/medications (apps/web/app/(app)/(admin)/dashboard/clients/[id]/edit/medications/page.tsx):
- Library tab: grouped + standalone medication cards; add/edit/view/delete dialogs; remove-from-group action (lines 275-345). Add/edit forms include drug search, dosage options, schedule builder (start time + recurrence serialized to RRULE in client timezone), stock/refill settings, and file attachments (
add-medication-form.tsx,edit-medication-form.tsx,medication-schedule-fields.tsx,medication-attachment-form.tsx). - Calendar tab: client-side RRULE expansion (via the
rrulepackage) renders upcoming dose occurrences (medication-assignment-calendar.tsx:86-170). - Log tab: paginated medication administration history with date/status/medication filters via
GET /clients/:id/medication-logs(medication-log-tab.tsx:45-46;apps/web/lib/api/client/medication-logs.ts). - Pharmacy section on the same page edits the client's pharmacies/home-health providers — this writes to the client care-providers record (
PUT /clients/:id/care-providers), not to medication entities (pharmacy-section.tsx:45-70). - Shift details show a per-shift medication summary from
GET /clients/:id/medication-logs/shift/:shiftAssignmentId(apps/web/.../shifts/_components/shift-medication-summary.tsx:4, 27). - FDA guide sections are rendered by
medication-guide-display.tsx.
There are no role checks specific to these pages beyond the dashboard's admin route group; the backend likewise applies none (see Business Rules).
Mobile — care providers
- During shifts:
MEDICATION_REMINDERtasks appear in the shift task list; the task bottom sheet shows a read-onlyMedicationDetailsCarduntil the task is started, then the interactiveMedicationChecklist(apps/mobile/components/care-provider-tasks/task-details-bottom-sheet.tsx:1007-1019). Checklist completion is required before the task can be submitted with the checklist payload (lines 205-210, 378-391). - Client medication list (read-only):
(client)/[id]/medications.tsxfetchesGET /clients/:id/medicationsand renders cards with parsed RRULE schedule text, guide, and files (apps/mobile/app/(client)/[id]/medications.tsx:552-570, 44).
Mobile — representatives (family)
- A dedicated Medications tab is configured for
UserRole.Representativeonly (apps/mobile/lib/tab-config.ts:99, 188-194). The screen (apps/mobile/app/(app)/(tabs)/medications/index.tsx, byte-identical copy at(app)/(medications)/index.tsx) is read-only: Active/All/Past filter tabs, stock status, instructions, FDA guide, and attachments for the representative's linked client (useProfileStore().getSelectedRepresentative().client, lines 266-285). It shows an "awaiting verification" state when no client is linked (line 483-484). - Representatives also receive
MedicationStockLowpush notifications (listener recipients, above).
No mobile surface can create, edit, or delete medications — the mobile API wrapper only exposes GETs (apps/mobile/lib/medications-api.ts).
Cross-Module Dependencies
- medications → care plans & tasks (04-care-plans-and-tasks.md): medication tasks are virtual — computed per shift read from
ClientMedication.schedulesand appended to the care-plan task list asmedgrp_<taskGroupId>_<scheduleId>tasks (client-care-provider-tasks.service.ts:843-861); stored legacyMEDICATION_REMINDERcare-plan tasks are filtered out (lines 827-833); task-submission resolution falls back toparseMedicationTaskId(client-task-submissions.service.ts:122-150); AI task generation explicitly skips medication tasks (care-provider-tasks-orchestrator.service.ts:360-361). - medications → scheduling & shifts (05-scheduling-and-shifts.md): occurrences are resolved against the shift window via
resolveRRuleOccurrence(compute-medication-tasks.util.ts:97-104); eachMedicationLogreferences theShiftAssignment. - task submissions → medications: completing a
MEDICATION_REMINDERsubmission emitsMEDICATION_TASK_COMPLETED_EVENT(client-task-submissions.service.ts:375-383, 755-767; event class inapps/backend/src/notification/events/medication-stock.events.ts). - medications → notifications:
MedicationStockNotificationListenerconsumes that event to write logs, decrement stock, and sendNotificationMedicationType.MedicationStockLowunified notifications (medication-stock-notification.listener.ts). - medications → files (LockCare/S3): attachments are LockCare documents; CRUD on medications cascades file create/delete (
client-medications.service.ts:554-631, 687-708, 808-949). - medications → shift summaries: shift-summary generation aggregates the shift's
MedicationLogrows into a per-medication compliance section (taken/skipped/unavailable counts, compliance rate) (apps/backend/src/clients/services/shift-summary.service.ts:220-251, 736-800). - medications → reporting/health metrics: period reports count medication refusals (
SKIPPED+CLIENT_REFUSED) and flag compliance < 70% (apps/backend/src/health-metrics/period-report-data.service.ts:751-755;report-data-enricher.service.ts:379-383); several PDF layouts render medication data (apps/backend/src/pdf/pdf-layout/*.pdf.tsx). - medications → platform logs: update/delete emit
PLATFORM_LOG_ACTIVITY_EVENT; list reads are PHI-audited (client-medications.service.ts:53-75;clients.controller.ts:234). - medications → client lifecycle: client cleanup deletes
MedicationLogrows among orphaned collections (apps/backend/src/clients/services/client-cleanup.service.ts:55-87). - External services: RxTerms (
clinicaltables.nlm.nih.gov) and OpenFDA NDC for search; OpenFDA drug-label API for guides (medication-search.service.ts:17-18;medication-label.service.ts:8).
Open Questions & Gaps
- No role/permission restrictions on medication CRUD. The endpoints carry only the controller-level
JwtAuthGuard— no@Rolesor@RequirePermissions(clients.controller.ts:233-314, class guard at line 92). Any authenticated user in the business (including CareProviders and Representatives) could create, edit, or delete medications via the API; restrictions are UI-only. Cannot determine from code whether this is intentional. medication.updated/medication.deletedevents are dead. They are emitted (client-medications.service.ts:658-661, 711-714, comments say "to sync with care provider tasks") but no@OnEventlistener exists anywhere in the backend. Sync now happens implicitly because tasks are computed at read time — the events appear to be leftovers.- Omitted checklist entries default to
TAKEN. If a caregiver submits a partial checklist, every group medication missing from it is logged as taken and its stock decremented (medication-stock-notification.listener.ts:94). Conversely, an entirely empty checklist produces no logs at all (client-task-submissions.service.ts:1381-1382) — the same shift task can therefore yield "all taken" or "nothing recorded" depending on payload shape. (The mobile UI requires a full checklist, so this is reachable mainly via API/web.) - No platform activity log or domain event on medication create — only update and delete are logged (
client-medications.service.ts:663-668, 718-723vs. nothing increateMedication). Audit trail for who added a medication relies solely on Mongo timestamps. - File
uploadedByis set to the client id, not the acting user. Both file-association helpers passclientIdasuserIdwith an inline comment "should be from auth context" (client-medications.service.ts:464, 620). GET /clients/:clientId/medication-logs/shift/:shiftAssignmentIdignoresclientId— the handler queries by shift assignment only (clients.controller.ts:308-314;medication-log.service.ts:82-92). Tenancy is still enforced by the business-scope plugin, but the URL's client id is decorative.isActivequery-param coercion is broken.ClientMedicationQueryDtouses@Type(() => Boolean)(client-medication.dto.ts:211-214), so the string"false"coerces totrue—?isActive=falsecannot actually filter for inactive medications server-side. Mobile filters Active/Past client-side, masking this.occurredAtis the submission time, not the scheduled dose time (medication-stock-notification.listener.ts:84, 161). Late completions record when the caregiver tapped, and there is no record of the intended dose time beyondscheduleId. Compliance reporting therefore reflects logging time. Cannot determine from code whether this is intended.- No missed-dose detection. Doses that fall in a shift but are never completed produce no
MedicationLogat all; compliance rates are computed only over logged doses (shift-summary.service.ts:780), so a shift where nothing was logged shows no medication section rather than 0% compliance. - FDA guide is fetched by name match only and overwrites silently. Brand/generic exact-phrase search can attach the wrong product's label for ambiguous names; renaming a medication or calling fetch-guide replaces the stored guide with no versioning (
medication-label.service.ts:33-36;client-medications.service.ts:798-799).guideis alsoSchema.Types.Mixedwith no validation, and the create/update DTOs accept anyguidepayload (client-medication.dto.ts:90-91). - Group-schedule validation asymmetry. Create rejects mismatched group schedules with a 400, but update silently overwrites every sibling's schedule (
client-medications.service.ts:348, 635-643). Two admins editing different group members last-write-wins on the whole group's schedule. - Stock decrement is once-per-completion, not idempotent per dose. Nothing prevents a second submission for the same
medgrp_task occurrence from inserting duplicate logs and decrementing stock again (no uniqueness on{taskSubmission}or{taskId, shiftAssignment}); whether re-submission is blocked upstream in the task-submission flow cannot be fully determined from this module. - Duplicate mobile screens.
(app)/(tabs)/medications/index.tsxand(app)/(medications)/index.tsxare identical 836-line copies maintained in two route groups (legacy(tabs)config is marked "TODO: Remove after migration",tab-config.ts:161). - Empty
ClientMedicationsModule(apps/backend/src/client-medications/client-medications.module.ts) suggests an unfinished extraction of medication code out of the clients module. - Pagination quirk:
findMedicationsreturnsnextPage: min(page+1, pages)/prevPage: max(page-1, 1)so next/prev equal the current page at the edges rather than null (client-medications.service.ts:402-412).