MeshiTrack/docs/phases/phase-1-medicine-library.md

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

  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.

// 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, MedicineQuerySchema
    • CreateMedicineProductSchema, UpdateMedicineProductSchema

1.2 -- Medicine Mongoose Schema & Repository

  • packages/api/src/modules/medicines/medicines.repository.ts
  • 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

  • packages/api/src/modules/medicine-products/medicine-products.repository.ts
  • 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

  • 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
  • MedicineProductsService with business logic:
    • Validate medicineId exists on create
    • Denormalize medicineName
  • Route plugins for both modules, registered via fp()

1.5 -- Web UI: Medicine Library

  • /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
  • 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.