MeshiTrack/docs/phases/phase-2-medicine-cabinet.md

7.9 KiB

Phase 2 — Medicine Cabinet

Goal: Track medicine inventory — what you have, how much of each, and when it expires. Provide aggregate views and low-stock/expiry warnings.

Depends on: Phase 0, Phase 1 (medicines)


Deliverables

  1. CabinetItem MongoDB schema and full CRUD API
  2. Aggregate quantity view per medicine
  3. Expiry date tracking and warnings
  4. Low stock alerts (based on configurable thresholds)
  5. Medicine cabinet web UI with status indicators

Data Model

CabinetItem Schema

// packages/shared/src/types/cabinet.ts
export interface CabinetItem {
  id: string;
  householdId: string;
  medicineId: string;
  medicineName: string; // Denormalized
  medicineStrength: number; // Denormalized for display
  medicineStrengthUnit: StrengthUnit; // Denormalized
  medicineForm: MedicineForm; // Denormalized
  quantity: number;
  unit: DosageUnit;
  expirationDate?: Date;
  lotNumber?: string;
  purchaseDate?: Date;
  purchasePrice?: number;
  storeId?: string;
  storeName?: string; // Denormalized
  status: CabinetItemStatus;
  notes?: string;
  createdBy: string;
  createdAt: Date;
  updatedAt: Date;
}

export enum DosageUnit {
  PILL = 'pill',
  CAPSULE = 'capsule',
  ML = 'ml',
  G = 'g',
  PATCH = 'patch',
  DOSE = 'dose',
  PUFF = 'puff',
  DROP = 'drop',
  APPLICATION = 'application',
}

export enum CabinetItemStatus {
  ACTIVE = 'active',
  DEPLETED = 'depleted',
  EXPIRED = 'expired',
  DISCARDED = 'discarded',
}

CabinetSummary (Computed, not stored)

// Aggregate view — total per medicine across all cabinet items
export interface CabinetSummary {
  medicineId: string;
  medicineName: string;
  medicineStrength: number;
  medicineStrengthUnit: StrengthUnit;
  medicineForm: MedicineForm;
  totalQuantity: number;
  unit: DosageUnit;
  earliestExpiry: Date | null;
  itemCount: number; // How many cabinet items (bottles/boxes)
  lowStockThreshold?: number; // From household settings
  isLowStock: boolean;
}

MongoDB Indexes

{ householdId: 1, medicineId: 1, status: 1 }
{ householdId: 1, status: 1 }
{ householdId: 1, expirationDate: 1 }          // For expiry warnings
{ householdId: 1, 'quantity': 1 }

API Endpoints

CabinetModule

Method Path Description Auth
GET /cabinet List cabinet items (filtered, paginated) member
GET /cabinet/summary Aggregate quantities per medicine member
GET /cabinet/:id Get single cabinet item member
POST /cabinet Add item to cabinet member
PATCH /cabinet/:id Update item (quantity, notes, etc.) member
POST /cabinet/:id/adjust Adjust quantity (add/subtract without full edit) member
DELETE /cabinet/:id Hard delete (admin) admin
GET /cabinet/expiring-soon Items expiring within N days member
GET /cabinet/low-stock Medicines below threshold quantity member

Query Parameters for GET /cabinet

?medicineId=abc123            # Filter by medicine
&status=active                # Filter by status
&expiringWithin=30            # Days until expiry
&sort=-expirationDate|name    # Sort field
&cursor=abc123
&limit=20

Adjust Quantity Request

// POST /cabinet/:id/adjust
interface AdjustQuantityRequest {
  delta: number; // Positive to add, negative to subtract
  reason?: string; // e.g., "Correcting count", "Dropped a pill"
}

Tasks

2.1 — Shared Types & Validation

  • Add cabinet types to packages/shared/src/types/cabinet.ts
  • Zod schemas:
    • CreateCabinetItemSchema
    • UpdateCabinetItemSchema
    • AdjustQuantitySchema
    • CabinetQuerySchema

2.2 — Mongoose Schema & Repository

  • packages/api/src/modules/cabinet/cabinet.repository.ts
  • CabinetRepository with:
    • findByHousehold(householdId, query) — filtered, paginated
    • findById(id, householdId)
    • findByMedicine(householdId, medicineId) — all items for a medicine
    • getAggregateSummary(householdId) — MongoDB aggregation pipeline
    • create(data)
    • update(id, householdId, data)
    • adjustQuantity(id, householdId, delta) — atomic $inc
    • findExpiringSoon(householdId, withinDays)
    • delete(id, householdId)

2.3 — Cabinet Service

class CabinetService {
  /** Add item, denormalizing medicine fields */
  addItem(data: CreateCabinetItem): Promise<CabinetItem>;

  /** Adjust quantity with floor at 0, auto-set depleted status */
  adjustQuantity(id: string, householdId: string, delta: number, reason?: string): Promise<CabinetItem>;

  /** Get aggregate summary with low stock flags */
  getSummary(householdId: string): Promise<CabinetSummary[]>;

  /** Find items expiring within N days */
  getExpiringSoon(householdId: string, withinDays: number): Promise<CabinetItem[]>;

  /** Find medicines below low stock threshold */
  getLowStock(householdId: string): Promise<CabinetSummary[]>;

  /**
   * Deduct quantity from cabinet items for a medicine (used by Pill Organizer in Phase 3).
   * Uses FEFO (First Expiry, First Out) — draws from items with earliest expiry first.
   * Returns actual quantity deducted (may be less than requested if insufficient).
   */
  deductStock(householdId: string, medicineId: string, quantity: number): Promise<DeductionResult>;

  /** Reverse a deduction (used by Pill Organizer undo) */
  restoreStock(householdId: string, cabinetItemId: string, quantity: number): Promise<CabinetItem>;
}

interface DeductionResult {
  totalDeducted: number;
  requested: number;
  isShort: boolean;
  deductions: {
    cabinetItemId: string;
    quantityTaken: number;
    remainingInItem: number;
  }[];
}

2.4 — Expiry Check Job

  • Scheduled job (daily at 6 AM, configurable):
    1. Query all active cabinet items with expirationDate <= today
    2. Update status to expired
    3. Create in-app notifications for expired items
    4. Query items expiring within 7 days, create warning notifications

2.5 — Web UI: Medicine Cabinet

  • /cabinet page:
    • Summary view (default): aggregated per medicine
      • Medicine name, total quantity, earliest expiry, low stock indicator
      • Expand to see individual items (bottles/boxes)
    • Detail view: all individual cabinet items
    • Each item shows: medicine name, quantity, expiry date, status badge
    • Color-coded expiry: green (>30 days), yellow (7-30 days), red (<7 days), grey (expired)
    • Low stock badge on medicines below threshold
    • Quick actions: adjust quantity (+/-), discard
    • "Add to Cabinet" button -> modal:
      • Medicine autocomplete (from library)
      • Quantity + unit
      • Expiration date (optional)
      • Lot number (optional)
      • Purchase date, price, store (optional)
  • /cabinet/alerts or notification panel:
    • Expiring soon items
    • Low stock warnings

Acceptance Criteria

  • Can add items to cabinet linked to medicines
  • Aggregate summary shows total quantity per medicine
  • Quantity adjustments are atomic and floor at 0
  • Items auto-transition to depleted when quantity reaches 0
  • Items auto-transition to expired when past expiration date
  • Expiring-soon endpoint returns items within N days
  • Low-stock endpoint compares against configurable thresholds
  • FEFO deduction draws from earliest-expiring items first
  • Web UI shows color-coded expiry indicators
  • All cabinet queries are scoped to householdId

Estimated Effort

Medium. CRUD with aggregation pipeline, FEFO logic, and scheduled expiry job. Simpler than food pantry tracking (no freshness estimation).