9.2 KiB
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
MedicineandMedicineProductMongoDB schemas with full CRUD APIs- Full-text search with category/form filters
- Cascade delete protection (cannot delete medicine with linked products)
- 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.
// 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.
// 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
// 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
interface PaginatedResponse<T> {
data: T[];
pagination: {
cursor: string | null; // null = last page
hasMore: boolean;
total: number;
};
}
Tasks
1.1 -- Shared Types & Validation
- Add Medicine types to
packages/shared/src/types/medicine.ts - Add MedicineProduct types (same file)
- Add enums to
packages/shared/src/enums/medicine.enums.ts - Create Zod schemas:
CreateMedicineSchema,UpdateMedicineSchema,MedicineQuerySchemaCreateMedicineProductSchema,UpdateMedicineProductSchema
1.2 -- Medicine Mongoose Schema & Repository
packages/api/src/modules/medicines/medicines.repository.tsMedicinesRepositorywith:findByHousehold(householdId, query)-- supports text search, filters, cursor paginationfindById(id, householdId)findDuplicate(householdId, name, strength, strengthUnit, form, excludeId?)create(data)update(id, householdId, data)softDelete(id, householdId)
1.3 -- MedicineProduct Mongoose Schema & Repository
packages/api/src/modules/medicine-products/medicine-products.repository.tsMedicineProductsRepositorywith:findByMedicine(householdId, medicineId, query)-- paginatedfindById(id, householdId)countByMedicineId(medicineId)-- for cascade delete checkcreate(data)update(id, householdId, data)softDelete(id, householdId)
1.4 -- Services & Routes
MedicinesServicewith 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
MedicineProductsServicewith business logic:- Validate medicineId exists on create
- Denormalize medicineName
- Route plugins for both modules, registered via
fp()
1.5 -- Web UI: Medicine Library
/medicinespage:- 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
- Add/Edit medicine form
- Add/Edit product form (nested under a medicine detail page)
- Brand, manufacturer, package size, concentration (vials only)
- Delete with confirmation (blocked if products exist)
Acceptance Criteria
- Can create, read, update, delete medicines (generic level) via API
- Can create, read, update, delete medicine products (purchasable level) via API
- Medicine products are correctly linked to their parent medicine
- Full-text search returns relevant results
- Category and form filters work on the medicines list
- Cascade delete check prevents deleting medicines with linked products
- Web UI shows two-level hierarchy: medicines with nested products
- All queries are scoped to
householdId - Dedup check prevents creating duplicate medicines (same name + strength + unit + form)
- 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.