# Phase 3 — Regimens, Pill Organizer & Cabinet Activity Log

**Goal**: Define daily medication schedules (regimens), batch-dispense from the medicine cabinet into a pill organizer, and track every cabinet mutation in an append-only audit log. The audit log provides the financial foundation for spending projections (Phase 4 enriches with formal price surveillance).

**Depends on**: Phase 0, Phase 1 (medicines), Phase 2 (cabinet)

---

## Deliverables

1. `CabinetEvent` append-only audit log for all cabinet mutations
2. `Regimen` MongoDB schema and CRUD API
3. `OrganizerFill` schema and fill/undo API
4. Pill organizer fill flow with shortage detection and FEFO allocation
5. Burn rate calculation with spending projections
6. Cabinet discard endpoint (zero quantity + soft-delete)
7. Regimen, pill organizer, and cabinet activity web UI

---

## Data Model

### CabinetEvent Schema (new collection, append-only)

Every mutation to a cabinet item is recorded as a `CabinetEvent`. This provides the audit trail, financial history, and data for spending projections.

```typescript
// packages/shared/src/types/cabinet-event.ts
export interface CabinetEvent {
  id: string;
  householdId: string;
  userId: string;
  cabinetItemId: string;
  medicineId: string;
  medicineName: string;              // Denormalized
  eventType: CabinetEventType;
  quantity: number;                  // Signed: positive=added, negative=removed
  quantityBefore: number;
  quantityAfter: number;
  unitPrice?: number;                // PURCHASED events only
  totalPrice?: number;
  currency?: string;
  storeId?: string;                  // Optional string reference (formal Store entity in Phase 4)
  storeName?: string;                // Denormalized
  sourceType: CabinetEventSourceType;
  sourceId?: string;                 // OrganizerFill ID, etc.
  reason?: string;                   // User-provided for adjust/discard
  notes?: string;
  createdAt: Date;
}

export enum CabinetEventType {
  PURCHASED = 'purchased',           // Cabinet item added (via addItem)
  CONSUMED = 'consumed',             // Deducted by organizer fill
  ADJUSTED = 'adjusted',             // Manual quantity adjustment
  DISCARDED = 'discarded',           // Item discarded (zero qty + soft-delete)
  RESTORED = 'restored',             // Reversed by organizer undo
  DELETED = 'deleted',               // Soft-deleted from cabinet
}

export enum CabinetEventSourceType {
  MANUAL = 'manual',
  ORGANIZER_FILL = 'organizer_fill',
  ORGANIZER_UNDO = 'organizer_undo',
  REFILL_LIST = 'refill_list',       // Phase 4
}

export interface SpendingSummary {
  byMedicine: SpendingByMedicine[];
  byPeriod: SpendingByPeriod[];
  total: number;
  currency: string | null;
}

export interface SpendingByMedicine {
  medicineId: string;
  medicineName: string;
  totalSpent: number;
  totalQuantity: number;
  avgUnitPrice: number;
  purchaseCount: number;
}

export interface SpendingByPeriod {
  period: string;
  totalSpent: number;
}
```

#### Event Emission Strategy

| Mutation | Event Type | Source Type |
|----------|-----------|-------------|
| `cabinet.addItem()` | PURCHASED | manual |
| `cabinet.update()` (qty change) | ADJUSTED | manual |
| `cabinet.adjustQuantity()` | ADJUSTED | manual |
| `cabinet.discard()` | DISCARDED | manual |
| `cabinet.delete()` | DELETED | manual |
| `organizer.fill()` | CONSUMED (per deduction) | organizer_fill |
| `organizer.undoFill()` | RESTORED (per deduction) | organizer_undo |

Events are logged **after** the primary mutation succeeds (not inside transactions for fill/undo -- events are audit records, not source of truth).

#### MongoDB Indexes

```javascript
// CabinetEvent
{ householdId: 1, createdAt: -1 }
{ householdId: 1, cabinetItemId: 1, createdAt: -1 }
{ householdId: 1, medicineId: 1, createdAt: -1 }
{ householdId: 1, eventType: 1, createdAt: -1 }
```

### CabinetItem Enhancement

Add optional purchase fields to existing `CabinetItem`:

```typescript
// Added to packages/shared/src/types/cabinet.ts
purchaseDate?: Date;
unitPrice?: number;
totalPrice?: number;
currency?: string;
storeId?: string;
storeName?: string;
```

### 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;
  medicineForm: MedicineForm;
  dosage: number;             // e.g., 2 (pills per dose)
  dosageUnit: DosageUnit;
  frequency: DosageFrequency;
  customFrequencyPerDay?: number;
  timeOfDay?: TimeOfDay;
  instructions?: string;
}

export enum DosageFrequency {
  DAILY = 'daily',
  TWICE_DAILY = 'twice_daily',
  THREE_TIMES_DAILY = 'three_times_daily',
  WEEKLY = 'weekly',
  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;
  fillDate: Date;
  items: OrganizerFillItem[];
  status: OrganizerFillStatus;
  notes?: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface OrganizerFillItem {
  medicineId: string;
  medicineName: string;
  quantityNeeded: number;
  quantityTaken: number;
  wasShort: boolean;
  shortage: number;
  deductions: OrganizerDeduction[];
}

export interface OrganizerDeduction {
  cabinetItemId: string;
  quantityTaken: number;
}

export enum OrganizerFillStatus {
  COMPLETED = 'completed',
  PARTIAL = 'partial',
  REVERSED = 'reversed',
}
```

### BurnRate (Computed, not stored)

```typescript
export interface BurnRate {
  medicineId: string;
  medicineName: string;
  dailyConsumption: number;
  totalInCabinet: number;
  daysUntilEmpty: number | null;
  earliestExpiry: Date | null;
  avgUnitPrice: number | null;         // Weighted avg from PURCHASED events
  projectedDailyCost: number | null;   // avgUnitPrice * dailyConsumption
  projectedMonthlyCost: number | null; // daily * 30
  projectedYearlyCost: number | null;  // daily * 365
  currency: string | null;
}
```

### Discard Behavior

Discarding a cabinet item: zero the quantity **and** soft-delete (`isDeleted=true`). The item disappears from the active cabinet list. A DISCARDED event preserves the audit trail (what was discarded, why, how much).

### 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

### CabinetEventsModule (new)

| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/cabinet-events` | List events (filtered by medicineId, eventType, dateRange) | member |
| GET | `/cabinet-events/by-item/:cabinetItemId` | Events for a single cabinet item | member |
| GET | `/cabinet-events/spending-summary` | Aggregated spending per medicine and period | member |

### CabinetModule (modifications)

| Method | Path | Description | Auth |
|--------|------|-------------|------|
| POST | `/cabinet/:id/discard` | Zero qty + soft-delete + DISCARDED event. Body: `{ reason, notes? }` | member |

All existing mutation endpoints (`POST /cabinet`, `PATCH /cabinet/:id`, `POST /cabinet/:id/adjust`, `DELETE /cabinet/:id`) now emit CabinetEvents. The `addItem` endpoint accepts optional purchase fields (`unitPrice`, `totalPrice`, `currency`, `storeId`, `storeName`).

### RegimensModule (new)

| 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 + spending projection | member |

### OrganizerModule (new)

| 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;
}

interface OrganizerPreviewResponse {
  regimenName: string;
  numberOfDays: number;
  items: {
    medicineId: string;
    medicineName: string;
    quantityNeeded: number;
    quantityAvailable: number;
    isShort: boolean;
    shortage: number;
    cabinetBreakdown: {
      cabinetItemId: string;
      expirationDate: string | null;
      quantityToTake: number;
      quantityBefore: number;
    }[];
  }[];
  canFillCompletely: boolean;
  hasShortages: boolean;
}
```

### Fill Request

```typescript
// POST /organizer/fill
interface OrganizerFillRequest {
  regimenId: string;
  numberOfDays: number;
  allowPartial: boolean;
  notes?: string;
}
```

---

## 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(
  dosage: number,
  frequency: DosageFrequency,
  numberOfDays: number,
  customFrequencyPerDay?: number,
): number;
```

Implemented as a pure function in `packages/shared/src/utils/frequency.ts`.

---

## Spending Projection

In `GET /regimens/burn-rate`:

1. Sum daily consumption per medicine from active regimens
2. Get cabinet stock per medicine
3. Query PURCHASED events for weighted average unit price per medicine
4. `projectedDailyCost = avgUnitPrice * dailyConsumption`
5. Monthly = daily * 30, yearly = daily * 365
6. Return `null` for medicines with no purchase price data

---

## Tasks

### 3.0 -- Phase 3 Spec Doc
- Write/update `docs/phases/phase-3-regimens-pill-organizer.md` with revised scope

### 3.1 -- Shared Enums
- `packages/shared/src/enums/cabinet-event.enums.ts` -- `CabinetEventType`, `CabinetEventSourceType`
- `packages/shared/src/enums/regimen.enums.ts` -- `DosageFrequency`, `TimeOfDay`, `OrganizerFillStatus`
- Update barrel, write tests

### 3.2 -- Shared Types
- `packages/shared/src/types/cabinet-event.ts`
- `packages/shared/src/types/regimen.ts`
- `packages/shared/src/types/organizer-fill.ts`
- `packages/shared/src/types/burn-rate.ts`
- Modify `packages/shared/src/types/cabinet.ts` -- add purchase fields
- Update barrel

### 3.3 -- Shared Validation Schemas
- `packages/shared/src/validation/cabinet-event.schemas.ts`
- `packages/shared/src/validation/regimen.schemas.ts`
- `packages/shared/src/validation/organizer.schemas.ts`
- Modify `packages/shared/src/validation/cabinet.schemas.ts` -- add purchase fields
- Update barrel, write tests

### 3.4 -- Frequency Multiplier Utility
- `packages/shared/src/utils/frequency.ts` -- pure functions
- `packages/shared/src/utils/index.ts` barrel
- Tests

### 3.5 -- CabinetEvent Schema + Repository + Service + Routes
- Mongoose schema with indexes
- Repository with create, find, aggregation methods
- Service as thin wrapper
- Routes: list, by-item, spending-summary

### 3.6 -- Modify Cabinet for Events + Discard
- Add purchase fields to cabinet Mongoose schema
- Add `discard()` and `findActiveByMedicineForFEFO()` to repository
- Inject `cabinetEventsService` into `CabinetService`
- Emit events on all mutations (add, update, adjust, delete, discard)
- Add `POST /:id/discard` endpoint

### 3.7 -- Regimen Schema + Repository + Service + Routes
- Mongoose schema with embedded medications
- Repository with CRUD scoped to householdId + userId
- Service with CRUD + medicine validation/denormalization + `calculateBurnRates()`
- Routes for all CRUD + burn-rate endpoint

### 3.8 -- OrganizerFill Schema + Repository + Service + Routes
- Mongoose schema with embedded items/deductions
- Repository with CRUD + status update
- Service with preview (FEFO), fill (transactional), undoFill (transactional)
- Routes for all 5 endpoints

### 3.9 -- Register in main.ts
- Add `cabinetEventsRoutes`, `regimensRoutes`, `organizerRoutes`

### 3.10 -- Web UI: Regimens
- Regimen list/detail views
- Add/edit regimen forms with medication management
- Active/inactive toggle

### 3.11 -- Web UI: Pill Organizer
- Fill flow: regimen select, day count, preview, fill
- Burn rate table with spending projections
- Fill history with undo

### 3.12 -- Web UI: Cabinet Activity
- Timeline of CabinetEvents
- Filter by medicine, event type, date range
- Spending summary view

---

## Impact on Phase 4

Phase 4 (Stores, Prices, Refills) scope unchanged -- it still provides:
- Formal `Store` entity CRUD
- `MedicinePriceRecord` for price surveillance (observed prices, not just purchases)
- Refill alerts + lists
- Spending analytics dashboards (enriched by CabinetEvent PURCHASED data)

The `storeId`/`storeName` on CabinetEvent are optional strings in Phase 3. They reference Store documents once Phase 4 is built.

---

## Acceptance Criteria

- [ ] Every cabinet mutation (add, update, adjust, discard, delete, fill, undo) creates a CabinetEvent
- [ ] Cabinet items can be created with optional purchase price data
- [ ] Discard zeros quantity, soft-deletes, and logs DISCARDED event
- [ ] 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 and logs RESTORED events
- [ ] Undo is idempotent (cannot undo an already-reversed fill)
- [ ] Burn rate correctly accounts for all active regimens with spending projections
- [ ] `as_needed` frequency is excluded from fill calculations and burn rate
- [ ] Spending summary aggregates PURCHASED events by medicine and period
- [ ] All queries scoped to `householdId`; regimens additionally scoped to `userId`
