264 lines
9.2 KiB
Markdown
264 lines
9.2 KiB
Markdown
# Phase 1 — Medicine Library
|
|
|
|
**Goal**: A two-level catalog of medicines. The **Medicine** level represents the generic substance you take (what regimens reference). The **MedicineProduct** level represents a specific purchasable item from a brand/manufacturer (what you buy and track prices for). Cabinet inventory tracks current quantity at the Medicine level, with an optional link back to which product it came from.
|
|
|
|
**Depends on**: Phase 0 (auth, households, shared types)
|
|
|
|
---
|
|
|
|
## Deliverables
|
|
|
|
1. `Medicine` and `MedicineProduct` MongoDB schemas with full CRUD APIs
|
|
2. Full-text search with category/form filters
|
|
3. Cascade delete protection (cannot delete medicine with linked products)
|
|
4. Medicine library web UI (search, filter, add, edit, delete -- both levels)
|
|
|
|
---
|
|
|
|
## Data Model
|
|
|
|
### Medicine Schema (Generic Level)
|
|
|
|
The generic substance -- what you take. Regimens and cabinet items reference this.
|
|
|
|
```typescript
|
|
// packages/shared/src/types/medicine.ts
|
|
export interface Medicine {
|
|
id: string;
|
|
householdId: string;
|
|
name: string; // Display name, e.g., "Metformin" or "Vitamin D3"
|
|
form: MedicineForm;
|
|
strength: number; // e.g., 500
|
|
strengthUnit: StrengthUnit; // e.g., 'mg' (weight/count only)
|
|
category: MedicineCategory;
|
|
notes?: string;
|
|
tags: string[];
|
|
createdBy: string;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
export enum MedicineForm {
|
|
TABLET = 'tablet',
|
|
CAPSULE = 'capsule',
|
|
LIQUID = 'liquid',
|
|
CREAM = 'cream',
|
|
INJECTION = 'injection',
|
|
INHALER = 'inhaler',
|
|
PATCH = 'patch',
|
|
DROPS = 'drops',
|
|
POWDER = 'powder',
|
|
SUPPOSITORY = 'suppository',
|
|
OTHER = 'other',
|
|
}
|
|
|
|
export enum StrengthUnit {
|
|
MG = 'mg',
|
|
MCG = 'mcg',
|
|
G = 'g',
|
|
ML = 'ml',
|
|
IU = 'IU',
|
|
PERCENT = '%',
|
|
OTHER = 'other',
|
|
}
|
|
|
|
export enum MedicineCategory {
|
|
PRESCRIPTION = 'prescription',
|
|
OTC = 'otc',
|
|
SUPPLEMENT = 'supplement',
|
|
OTHER = 'other',
|
|
}
|
|
```
|
|
|
|
### MedicineProduct Schema (Purchasable Level)
|
|
|
|
A specific brand/package you can buy. References a Medicine. Used for price tracking and ordering. Vial products can include concentration data.
|
|
|
|
```typescript
|
|
// packages/shared/src/types/medicine.ts
|
|
export interface MedicineProduct {
|
|
id: string;
|
|
householdId: string;
|
|
medicineId: string; // Reference to Medicine
|
|
medicineName: string; // Denormalized for display
|
|
brand: string; // e.g., "CVS Health", "Kirkland"
|
|
manufacturer?: string;
|
|
packageSize: number; // e.g., 90 (pills per bottle)
|
|
packageUnit: DosageUnit; // e.g., 'pill', 'ml', 'vial'
|
|
concentration?: number; // For vials only, e.g., 100
|
|
concentrationUnit?: ConcentrationUnit; // For vials only, e.g., 'units/mL'
|
|
imageUrl?: string;
|
|
notes?: string;
|
|
source: MedicineProductSource;
|
|
createdBy: string;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
export enum MedicineProductSource {
|
|
MANUAL = 'manual',
|
|
IMPORT = 'import',
|
|
}
|
|
|
|
export enum ConcentrationUnit {
|
|
MG_PER_ML = 'mg/mL',
|
|
MCG_PER_ML = 'mcg/mL',
|
|
UNITS_PER_ML = 'units/mL',
|
|
}
|
|
```
|
|
|
|
### Relationship Diagram
|
|
|
|
```
|
|
Medicine (generic) MedicineProduct (purchasable)
|
|
┌─────────────────────┐ ┌──────────────────────────────┐
|
|
│ Metformin 500mg tab │◄────────│ CVS Metformin 500mg, 90ct │
|
|
│ │◄────────│ Kirkland Metformin 500mg, 60ct│
|
|
└─────────────────────┘ └──────────────────────────────┘
|
|
▲ ▲
|
|
│ │
|
|
Referenced by: Referenced by:
|
|
- Regimens (Phase 3) - PriceRecords (Phase 4)
|
|
- CabinetItems (Phase 2) - CabinetItems (Phase 2, optional)
|
|
```
|
|
|
|
### MongoDB Indexes
|
|
|
|
```javascript
|
|
// Medicine
|
|
{ householdId: 1, name: 'text', tags: 'text' }
|
|
{ householdId: 1, category: 1 }
|
|
{ householdId: 1, name: 1, strength: 1, strengthUnit: 1, form: 1 } // dedup
|
|
|
|
// MedicineProduct
|
|
{ householdId: 1, medicineId: 1 }
|
|
{ householdId: 1, brand: 'text' }
|
|
```
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
### MedicinesModule (Generic Level)
|
|
|
|
| Method | Path | Description | Auth |
|
|
| ------ | --------------------- | --------------------------------- | ------ |
|
|
| GET | `/medicines` | List/search medicines (paginated) | member |
|
|
| GET | `/medicines/:id` | Get single medicine | member |
|
|
| POST | `/medicines` | Create medicine | member |
|
|
| PATCH | `/medicines/:id` | Update medicine | member |
|
|
| DELETE | `/medicines/:id` | Soft-delete medicine | admin |
|
|
| POST | `/medicines/import` | Bulk import from CSV/JSON | admin |
|
|
|
|
### MedicineProductsModule (Purchasable Level)
|
|
|
|
| Method | Path | Description | Auth |
|
|
| ------ | --------------------------------------- | ------------------------------------------ | ------ |
|
|
| GET | `/medicines/:medicineId/products` | List products for a medicine | member |
|
|
| GET | `/medicine-products/:id` | Get single product | member |
|
|
| POST | `/medicines/:medicineId/products` | Create product under a medicine | member |
|
|
| PATCH | `/medicine-products/:id` | Update product | member |
|
|
| DELETE | `/medicine-products/:id` | Soft-delete product | admin |
|
|
|
|
### Query Parameters for GET `/medicines`
|
|
|
|
```
|
|
?q=metformin # Full-text search
|
|
&category=prescription # Filter by category
|
|
&form=tablet # Filter by form
|
|
&tags=daily,morning # Filter by tags (AND)
|
|
&cursor=abc123 # Cursor-based pagination
|
|
&limit=20 # Page size (max 100)
|
|
&sort=name|-updatedAt # Sort field, prefix - for desc
|
|
```
|
|
|
|
### Response Shape
|
|
|
|
```typescript
|
|
interface PaginatedResponse<T> {
|
|
data: T[];
|
|
pagination: {
|
|
cursor: string | null; // null = last page
|
|
hasMore: boolean;
|
|
total: number;
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Tasks
|
|
|
|
### 1.1 -- Shared Types & Validation
|
|
|
|
- [x] Add Medicine types to `packages/shared/src/types/medicine.ts`
|
|
- [x] Add MedicineProduct types (same file)
|
|
- [x] Add enums to `packages/shared/src/enums/medicine.enums.ts`
|
|
- [x] Create Zod schemas:
|
|
- `CreateMedicineSchema`, `UpdateMedicineSchema`, `MedicineQuerySchema`
|
|
- `CreateMedicineProductSchema`, `UpdateMedicineProductSchema`
|
|
|
|
### 1.2 -- Medicine Mongoose Schema & Repository
|
|
|
|
- [x] `packages/api/src/modules/medicines/medicines.repository.ts`
|
|
- [x] `MedicinesRepository` with:
|
|
- `findByHousehold(householdId, query)` -- supports text search, filters, cursor pagination
|
|
- `findById(id, householdId)`
|
|
- `findDuplicate(householdId, name, strength, strengthUnit, form, excludeId?)`
|
|
- `create(data)`
|
|
- `update(id, householdId, data)`
|
|
- `softDelete(id, householdId)`
|
|
|
|
### 1.3 -- MedicineProduct Mongoose Schema & Repository
|
|
|
|
- [x] `packages/api/src/modules/medicine-products/medicine-products.repository.ts`
|
|
- [x] `MedicineProductsRepository` with:
|
|
- `findByMedicine(householdId, medicineId, query)` -- paginated
|
|
- `findById(id, householdId)`
|
|
- `countByMedicineId(medicineId)` -- for cascade delete check
|
|
- `create(data)`
|
|
- `update(id, householdId, data)`
|
|
- `softDelete(id, householdId)`
|
|
|
|
### 1.4 -- Services & Routes
|
|
|
|
- [x] `MedicinesService` with business logic:
|
|
- Dedup check on create (same name + strength + strengthUnit + form within household)
|
|
- Cascade: when deleting a medicine, check for linked products and block if any exist
|
|
- [x] `MedicineProductsService` with business logic:
|
|
- Validate medicineId exists on create
|
|
- Denormalize medicineName
|
|
- [x] Route plugins for both modules, registered via `fp()`
|
|
|
|
### 1.5 -- Web UI: Medicine Library
|
|
|
|
- [x] `/medicines` page:
|
|
- Search bar with text search
|
|
- Category and form filter dropdowns
|
|
- Medicine list view
|
|
- Each medicine card shows: name, strength + unit, form, category badge
|
|
- Click a medicine to see its detail page with products
|
|
- [x] Add/Edit medicine form
|
|
- [x] Add/Edit product form (nested under a medicine detail page)
|
|
- Brand, manufacturer, package size, concentration (vials only)
|
|
- [x] Delete with confirmation (blocked if products exist)
|
|
|
|
---
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [x] Can create, read, update, delete medicines (generic level) via API
|
|
- [x] Can create, read, update, delete medicine products (purchasable level) via API
|
|
- [x] Medicine products are correctly linked to their parent medicine
|
|
- [x] Full-text search returns relevant results
|
|
- [x] Category and form filters work on the medicines list
|
|
- [x] Cascade delete check prevents deleting medicines with linked products
|
|
- [x] Web UI shows two-level hierarchy: medicines with nested products
|
|
- [x] All queries are scoped to `householdId`
|
|
- [x] Dedup check prevents creating duplicate medicines (same name + strength + unit + form)
|
|
- [x] Vial products support concentration + concentration unit fields
|
|
---
|
|
|
|
## Estimated Effort
|
|
|
|
Medium. Two related CRUD modules with search. The two-level structure adds a bit of complexity over a flat model but keeps the domain clean.
|