Implement regimens
This commit is contained in:
parent
1f66fab30f
commit
9f416903ef
66 changed files with 9130 additions and 189 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Phase 3 — Regimens & Pill Organizer
|
||||
# Phase 3 — Regimens, Pill Organizer & Cabinet Activity Log
|
||||
|
||||
**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.
|
||||
**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)
|
||||
|
||||
|
|
@ -8,16 +8,123 @@
|
|||
|
||||
## 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
|
||||
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
|
||||
|
|
@ -25,8 +132,8 @@
|
|||
export interface Regimen {
|
||||
id: string;
|
||||
householdId: string;
|
||||
userId: string; // Regimens are per-person
|
||||
name: string; // e.g., "Daily medications", "Morning routine"
|
||||
userId: string; // Regimens are per-person
|
||||
name: string; // e.g., "Daily medications", "Morning routine"
|
||||
isActive: boolean;
|
||||
medications: RegimenMedication[];
|
||||
createdBy: string;
|
||||
|
|
@ -36,26 +143,26 @@ export interface Regimen {
|
|||
|
||||
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)
|
||||
medicineName: string; // Denormalized
|
||||
medicineStrength: number; // Denormalized
|
||||
medicineStrengthUnit: StrengthUnit;
|
||||
medicineForm: MedicineForm;
|
||||
dosage: number; // e.g., 2 (pills per dose)
|
||||
dosageUnit: DosageUnit;
|
||||
frequency: DosageFrequency;
|
||||
customFrequencyPerDay?: number; // When frequency is 'custom'
|
||||
customFrequencyPerDay?: number;
|
||||
timeOfDay?: TimeOfDay;
|
||||
instructions?: string; // e.g., "Take with food", "Do not crush"
|
||||
instructions?: string;
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
AS_NEEDED = 'as_needed', // Excluded from organizer fill calculations
|
||||
CUSTOM = 'custom', // Uses customFrequencyPerDay
|
||||
}
|
||||
|
||||
export enum TimeOfDay {
|
||||
|
|
@ -75,8 +182,8 @@ export interface OrganizerFill {
|
|||
householdId: string;
|
||||
userId: string;
|
||||
regimenId: string;
|
||||
regimenName: string; // Denormalized
|
||||
numberOfDays: number; // Flexible: 1, 6, 7, 14, etc.
|
||||
regimenName: string; // Denormalized
|
||||
numberOfDays: number;
|
||||
fillDate: Date;
|
||||
items: OrganizerFillItem[];
|
||||
status: OrganizerFillStatus;
|
||||
|
|
@ -88,10 +195,10 @@ export interface OrganizerFill {
|
|||
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)
|
||||
quantityNeeded: number;
|
||||
quantityTaken: number;
|
||||
wasShort: boolean;
|
||||
shortage: number;
|
||||
deductions: OrganizerDeduction[];
|
||||
}
|
||||
|
||||
|
|
@ -101,26 +208,34 @@ export interface OrganizerDeduction {
|
|||
}
|
||||
|
||||
export enum OrganizerFillStatus {
|
||||
COMPLETED = 'completed', // All medicines fully dispensed
|
||||
PARTIAL = 'partial', // Some medicines were short
|
||||
REVERSED = 'reversed', // Fill was undone
|
||||
COMPLETED = 'completed',
|
||||
PARTIAL = 'partial',
|
||||
REVERSED = 'reversed',
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
dailyConsumption: number;
|
||||
totalInCabinet: number;
|
||||
daysUntilEmpty: number | null; // null if dailyConsumption is 0
|
||||
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
|
||||
|
|
@ -138,26 +253,42 @@ export interface BurnRate {
|
|||
|
||||
## API Endpoints
|
||||
|
||||
### RegimensModule
|
||||
### CabinetEventsModule (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 for all active regimens | member |
|
||||
| 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 |
|
||||
|
||||
### OrganizerModule
|
||||
### CabinetModule (modifications)
|
||||
|
||||
| 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 |
|
||||
| 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
|
||||
|
||||
|
|
@ -165,7 +296,7 @@ export interface BurnRate {
|
|||
// POST /organizer/preview
|
||||
interface OrganizerPreviewRequest {
|
||||
regimenId: string;
|
||||
numberOfDays: number; // Default: 7
|
||||
numberOfDays: number;
|
||||
}
|
||||
|
||||
interface OrganizerPreviewResponse {
|
||||
|
|
@ -180,8 +311,9 @@ interface OrganizerPreviewResponse {
|
|||
shortage: number;
|
||||
cabinetBreakdown: {
|
||||
cabinetItemId: string;
|
||||
expirationDate: Date | null;
|
||||
expirationDate: string | null;
|
||||
quantityToTake: number;
|
||||
quantityBefore: number;
|
||||
}[];
|
||||
}[];
|
||||
canFillCompletely: boolean;
|
||||
|
|
@ -196,33 +328,14 @@ interface OrganizerPreviewResponse {
|
|||
interface OrganizerFillRequest {
|
||||
regimenId: string;
|
||||
numberOfDays: number;
|
||||
allowPartial: boolean; // If false, reject when any medicine is short
|
||||
allowPartial: boolean;
|
||||
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
|
||||
## Frequency Multiplier Logic
|
||||
|
||||
```typescript
|
||||
/**
|
||||
|
|
@ -237,118 +350,131 @@ interface OrganizerFillRequest {
|
|||
* custom: dosage * customFrequencyPerDay * numberOfDays
|
||||
*/
|
||||
function calculateQuantityNeeded(
|
||||
medication: RegimenMedication,
|
||||
dosage: number,
|
||||
frequency: DosageFrequency,
|
||||
numberOfDays: number,
|
||||
customFrequencyPerDay?: number,
|
||||
): number;
|
||||
```
|
||||
|
||||
### 3.4 — Organizer Fill Service
|
||||
Implemented as a pure function in `packages/shared/src/utils/frequency.ts`.
|
||||
|
||||
```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<OrganizerPreviewResponse>;
|
||||
---
|
||||
|
||||
/**
|
||||
* 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<OrganizerFill>;
|
||||
## Spending Projection
|
||||
|
||||
/**
|
||||
* 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<OrganizerFill>;
|
||||
}
|
||||
```
|
||||
In `GET /regimens/burn-rate`:
|
||||
|
||||
### 3.5 — Burn Rate Calculation
|
||||
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
|
||||
|
||||
```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<BurnRate[]>;
|
||||
}
|
||||
```
|
||||
---
|
||||
|
||||
### 3.6 — Web UI: Regimens
|
||||
## Tasks
|
||||
|
||||
- `/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.0 -- Phase 3 Spec Doc
|
||||
- Write/update `docs/phases/phase-3-regimens-pill-organizer.md` with revised scope
|
||||
|
||||
### 3.7 — Web UI: Pill Organizer
|
||||
### 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
|
||||
|
||||
- `/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)
|
||||
### 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
|
||||
- [ ] 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
|
||||
- [ ] 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`
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue