# Phase 3 — Regimens & Pill Organizer **Goal**: Define daily medication schedules (regimens) and batch-dispense from the medicine cabinet into a pill organizer. This is the core convenience feature: instead of tracking individual pill consumption daily, users fill their organizer for N days in a single action. **Depends on**: Phase 0, Phase 1 (medicines), Phase 2 (cabinet) --- ## Deliverables 1. `Regimen` MongoDB schema and CRUD API 2. `OrganizerFill` schema and fill/undo API 3. Pill organizer fill flow with shortage detection 4. Burn rate calculation (days until empty per medicine) 5. Regimen and pill organizer web UI --- ## Data Model ### Regimen Schema ```typescript // packages/shared/src/types/regimen.ts export interface Regimen { id: string; householdId: string; userId: string; // Regimens are per-person name: string; // e.g., "Daily medications", "Morning routine" isActive: boolean; medications: RegimenMedication[]; createdBy: string; createdAt: Date; updatedAt: Date; } export interface RegimenMedication { medicineId: string; medicineName: string; // Denormalized medicineStrength: number; // Denormalized medicineStrengthUnit: StrengthUnit; // Denormalized medicineForm: MedicineForm; // Denormalized dosage: number; // e.g., 2 (pills per dose) dosageUnit: DosageUnit; frequency: DosageFrequency; customFrequencyPerDay?: number; // When frequency is 'custom' timeOfDay?: TimeOfDay; instructions?: string; // e.g., "Take with food", "Do not crush" } export enum DosageFrequency { DAILY = 'daily', // 1x per day TWICE_DAILY = 'twice_daily', // 2x per day THREE_TIMES_DAILY = 'three_times_daily', // 3x per day WEEKLY = 'weekly', // 1x per week EVERY_OTHER_DAY = 'every_other_day', AS_NEEDED = 'as_needed', // Excluded from organizer fill calculations CUSTOM = 'custom', // Uses customFrequencyPerDay } export enum TimeOfDay { MORNING = 'morning', AFTERNOON = 'afternoon', EVENING = 'evening', BEDTIME = 'bedtime', } ``` ### OrganizerFill Schema ```typescript // packages/shared/src/types/organizer-fill.ts export interface OrganizerFill { id: string; householdId: string; userId: string; regimenId: string; regimenName: string; // Denormalized numberOfDays: number; // Flexible: 1, 6, 7, 14, etc. fillDate: Date; items: OrganizerFillItem[]; status: OrganizerFillStatus; notes?: string; createdAt: Date; updatedAt: Date; } export interface OrganizerFillItem { medicineId: string; medicineName: string; quantityNeeded: number; // Total pills needed for N days quantityTaken: number; // Actual pills taken from cabinet wasShort: boolean; // quantityTaken < quantityNeeded shortage: number; // quantityNeeded - quantityTaken (0 if not short) deductions: OrganizerDeduction[]; } export interface OrganizerDeduction { cabinetItemId: string; quantityTaken: number; } export enum OrganizerFillStatus { COMPLETED = 'completed', // All medicines fully dispensed PARTIAL = 'partial', // Some medicines were short REVERSED = 'reversed', // Fill was undone } ``` ### BurnRate (Computed, not stored) ```typescript // Calculated from active regimens + cabinet stock export interface BurnRate { medicineId: string; medicineName: string; dailyConsumption: number; // Total pills per day across all regimens totalInCabinet: number; daysUntilEmpty: number | null; // null if dailyConsumption is 0 earliestExpiry: Date | null; } ``` ### MongoDB Indexes ```javascript // Regimen { householdId: 1, userId: 1, isActive: 1 } { householdId: 1, 'medications.medicineId': 1 } // OrganizerFill { householdId: 1, userId: 1, fillDate: -1 } { householdId: 1, regimenId: 1, fillDate: -1 } { householdId: 1, status: 1 } ``` --- ## API Endpoints ### RegimensModule | Method | Path | Description | Auth | | ------ | ----------------------- | ------------------------------------ | ------ | | GET | `/regimens` | List user's regimens | member | | GET | `/regimens/:id` | Get single regimen | member | | POST | `/regimens` | Create regimen | member | | PATCH | `/regimens/:id` | Update regimen | member | | DELETE | `/regimens/:id` | Delete regimen | member | | GET | `/regimens/burn-rate` | Burn rate for all active regimens | member | ### OrganizerModule | Method | Path | Description | Auth | | ------ | ----------------------------- | -------------------------------------------- | ------ | | GET | `/organizer/fills` | List fill history (paginated) | member | | GET | `/organizer/fills/:id` | Get single fill details | member | | POST | `/organizer/preview` | Preview a fill (shows quantities, shortages) | member | | POST | `/organizer/fill` | Execute a fill (deduct from cabinet) | member | | POST | `/organizer/fills/:id/undo` | Reverse a fill (restore cabinet quantities) | member | ### Preview Request/Response ```typescript // POST /organizer/preview interface OrganizerPreviewRequest { regimenId: string; numberOfDays: number; // Default: 7 } interface OrganizerPreviewResponse { regimenName: string; numberOfDays: number; items: { medicineId: string; medicineName: string; quantityNeeded: number; quantityAvailable: number; isShort: boolean; shortage: number; cabinetBreakdown: { cabinetItemId: string; expirationDate: Date | null; quantityToTake: number; }[]; }[]; canFillCompletely: boolean; hasShortages: boolean; } ``` ### Fill Request ```typescript // POST /organizer/fill interface OrganizerFillRequest { regimenId: string; numberOfDays: number; allowPartial: boolean; // If false, reject when any medicine is short notes?: string; } ``` --- ## Tasks ### 3.1 — Shared Types & Validation - Add regimen types to `packages/shared/src/types/regimen.ts` - Add organizer fill types to `packages/shared/src/types/organizer-fill.ts` - Zod schemas: - `CreateRegimenSchema` - `UpdateRegimenSchema` - `OrganizerPreviewSchema` - `OrganizerFillSchema` ### 3.2 — Regimen CRUD - `RegimensRepository` and `RegimensService` - Standard CRUD scoped to `householdId` + `userId` - On create/update: validate that all `medicineId` references exist in the medicine library - Denormalize medicine fields (name, strength, unit, form) ### 3.3 — Frequency Multiplier Logic ```typescript /** * Calculate total pills needed for N days based on frequency. * * daily: dosage * numberOfDays * twice_daily: dosage * 2 * numberOfDays * three_times_daily: dosage * 3 * numberOfDays * weekly: dosage * ceil(numberOfDays / 7) * every_other_day: dosage * ceil(numberOfDays / 2) * as_needed: 0 (excluded from organizer fills) * custom: dosage * customFrequencyPerDay * numberOfDays */ function calculateQuantityNeeded( medication: RegimenMedication, numberOfDays: number, ): number; ``` ### 3.4 — Organizer Fill Service ```typescript class OrganizerService { /** * Preview: calculate what would happen without deducting. * For each medicine in the regimen: * 1. Calculate quantity needed (via frequency multiplier) * 2. Check cabinet stock (via CabinetService.getAggregateSummary) * 3. Plan FEFO deductions (earliest expiry first) * 4. Flag shortages */ preview(householdId: string, regimenId: string, numberOfDays: number): Promise; /** * Fill: execute the preview plan. * 1. Re-validate stock (may have changed since preview) * 2. If allowPartial=false and any shortage, reject * 3. Call CabinetService.deductStock for each medicine * 4. Create OrganizerFill record * 5. Return fill details */ fill(householdId: string, userId: string, request: OrganizerFillRequest): Promise; /** * Undo: reverse a fill. * 1. Verify fill is not already reversed * 2. For each deduction, call CabinetService.restoreStock * 3. Mark fill as reversed */ undoFill(householdId: string, fillId: string): Promise; } ``` ### 3.5 — Burn Rate Calculation ```typescript class BurnRateService { /** * For each medicine across all active regimens for a user: * 1. Sum daily consumption: dosage * daily_frequency_multiplier * 2. Get total cabinet stock for that medicine * 3. daysUntilEmpty = floor(totalInCabinet / dailyConsumption) * 4. Include earliest expiry date from cabinet * * Note: 'as_needed' frequency is excluded from burn rate. * Note: If multiple users in household have regimens, each sees their own burn rate. */ calculateBurnRates(householdId: string, userId: string): Promise; } ``` ### 3.6 — Web UI: Regimens - `/regimens` page: - List of user's regimens with active/inactive toggle - Each regimen shows: name, medication count, active status - Expand/click to see all medications with dosage details - Add/Edit regimen form: - Name, active toggle - Medications list: - Medicine autocomplete (from library) - Dosage (number + unit) - Frequency dropdown - Time of day (optional) - Instructions (optional) - Add/remove medications ### 3.7 — Web UI: Pill Organizer - `/organizer` page: - **Fill organizer** section: - Select regimen dropdown - Number of days input (default: 7, adjustable) - "Preview" button -> shows: - Per-medicine breakdown: needed vs available - Shortage warnings (highlighted) - Which cabinet items will be drawn from (FEFO order) - "Fill" button -> executes the fill, shows confirmation - Option for partial fill when shortages exist - **Burn rate** section: - Table: medicine name, daily consumption, total in cabinet, days until empty - Color-coded: green (>14 days), yellow (7-14 days), red (<7 days) - Links to refill alerts (Phase 4) - **Fill history** section: - Recent fills with date, regimen, day count, status - Expand to see per-medicine details - "Undo" button on recent fills (with confirmation) --- ## Acceptance Criteria - [ ] Can create and manage regimens with multiple medications - [ ] Frequency multiplier correctly calculates quantities for all frequency types - [ ] Preview accurately shows needed quantities and shortages - [ ] Fill deducts from cabinet using FEFO (earliest expiry first) - [ ] Partial fills work when `allowPartial` is true - [ ] Fill is rejected when `allowPartial` is false and any medicine is short - [ ] Undo fully restores cabinet quantities - [ ] Undo is idempotent (cannot undo an already-reversed fill) - [ ] Burn rate correctly accounts for all active regimens - [ ] `as_needed` frequency is excluded from fill calculations and burn rate - [ ] All queries scoped to `householdId`; regimens additionally scoped to `userId` --- ## Estimated Effort Medium-large. The fill/undo transactional logic with FEFO, shortage handling, and burn rate calculations are the most complex parts. UI is moderately complex with the preview/fill flow.