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