MeshiTrack/docs/phases/phase-4-pharmacies-prices-refills.md

11 KiB

Phase 4 — Pharmacies, Prices & Refills

Goal: Track where you buy medicines, compare prices across pharmacies, and get automatic refill alerts when cabinet stock is running low. The Store and PriceRecord infrastructure built here is shared with food tracking (Phase 9).

Depends on: Phase 0, Phase 1 (medicines), Phase 2 (cabinet), Phase 3 (regimens — for burn rate)


Deliverables

  1. Store MongoDB schema and CRUD API (shared infrastructure)
  2. PriceRecord schema for medicine price tracking
  3. Price history and store comparison
  4. Refill alerts based on burn rate
  5. Refill list generation
  6. Web UI: stores, price history, refill management

Data Model

Store Schema (Shared — used by both medicine and food domains)

// packages/shared/src/types/store.ts
export interface Store {
  id: string;
  householdId: string;
  name: string;
  address?: string;
  location?: {
    lat: number;
    lng: number;
  };
  url?: string;
  notes?: string;
  tags: string[]; // e.g., 'pharmacy', 'grocery', 'online', 'bulk', 'discount'
  isActive: boolean;
  createdBy: string;
  createdAt: Date;
  updatedAt: Date;
}

PriceRecord Schema (Medicine)

// packages/shared/src/types/medicine-price.ts
export interface MedicinePriceRecord {
  id: string;
  householdId: string;
  medicineId: string;
  medicineName: string; // Denormalized
  storeId: string;
  storeName: string; // Denormalized
  price: number;
  currency: string; // Default from household settings
  quantity: number; // How many pills/units for this price
  unit: DosageUnit;
  pricePerUnit: number; // Computed: price / quantity
  date: Date;
  isInsurancePrice: boolean; // With insurance vs retail
  notes?: string;
  createdBy: string;
  createdAt: Date;
}

RefillAlert (Computed, not stored)

// packages/shared/src/types/refill.ts
export interface RefillAlert {
  medicineId: string;
  medicineName: string;
  medicineStrength: number;
  medicineStrengthUnit: StrengthUnit;
  daysUntilEmpty: number;
  dailyConsumption: number;
  currentStock: number;
  suggestedQuantity: number; // Enough for N days (configurable, default 30)
  lastKnownPrice?: {
    price: number;
    pricePerUnit: number;
    storeName: string;
    storeId: string;
    date: Date;
  };
  cheapestOption?: {
    price: number;
    pricePerUnit: number;
    storeName: string;
    storeId: string;
    date: Date;
  };
}

export interface RefillList {
  id: string;
  householdId: string;
  name: string;
  items: RefillListItem[];
  status: RefillListStatus;
  preferredStoreId?: string;
  totalEstimatedCost?: number;
  createdBy: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface RefillListItem {
  id: string;
  medicineId: string;
  medicineName: string;
  quantity: number;
  unit: DosageUnit;
  estimatedPrice?: number;
  actualPrice?: number;
  checked: boolean;
  checkedAt?: Date;
  addedToCabinet: boolean;
  storeId?: string;
  notes?: string;
}

export enum RefillListStatus {
  ACTIVE = 'active',
  SHOPPING = 'shopping',
  COMPLETED = 'completed',
  ARCHIVED = 'archived',
}

MongoDB Indexes

// Store
{ householdId: 1, name: 1 }
{ householdId: 1, tags: 1 }

// MedicinePriceRecord
{ householdId: 1, medicineId: 1, storeId: 1, date: -1 }
{ householdId: 1, medicineId: 1, date: -1 }
{ householdId: 1, storeId: 1, date: -1 }

// RefillList
{ householdId: 1, status: 1 }
{ householdId: 1, createdAt: -1 }

API Endpoints

StoresModule (Shared)

Method Path Description Auth
GET /stores List stores for household member
GET /stores/:id Get single store member
POST /stores Add a store member
PATCH /stores/:id Update store member
DELETE /stores/:id Deactivate store admin

MedicinePricesModule

Method Path Description Auth
POST /medicine-prices Record a price member
GET /medicine-prices/history/:medicineId Price history for a medicine member
GET /medicine-prices/compare/:medicineId Compare stores for a medicine member
GET /medicine-prices/analytics Spending analytics member

RefillsModule

Method Path Description Auth
GET /refills/alerts Get refill alerts (medicines running low) member
POST /refills/lists Create refill list (manual or from alerts) member
GET /refills/lists List refill lists member
GET /refills/lists/:id Get refill list member
PATCH /refills/lists/:id Update refill list member
PATCH /refills/lists/:id/items/:itemId Check off / update item member
POST /refills/lists/:id/add-to-cabinet Move checked items to cabinet member
GET /refills/lists/:id/store-comparison Best store for this list member

Query Parameters

# GET /stores
?tags=pharmacy              # Filter by tags
&search=walgreens           # Name search

# GET /medicine-prices/history/:medicineId
?storeId=abc123             # Filter by store
&startDate=2026-01-01       # Date range
&endDate=2026-03-27
&limit=50

# GET /refills/alerts
?thresholdDays=7            # Alert when <= N days of stock remain (default: 7)
&userId=abc123              # Filter by user's regimens

Tasks

4.1 — Shared Types & Validation

  • Add store types to packages/shared/src/types/store.ts
  • Add medicine price types to packages/shared/src/types/medicine-price.ts
  • Add refill types to packages/shared/src/types/refill.ts
  • Zod schemas for all create/update operations

4.2 — Stores CRUD (Shared Infrastructure)

  • packages/api/src/modules/stores/
  • Standard CRUD, scoped to householdId
  • Tag-based filtering (pharmacy, grocery, online, etc.)
  • This module is used by both medicine and food domains

4.3 — Medicine Price Service

class MedicinePriceService {
  /** Record a price, computing pricePerUnit */
  recordPrice(data: CreateMedicinePriceRecord): Promise<MedicinePriceRecord>;

  /** Get price history for a medicine, optionally filtered by store */
  getPriceHistory(medicineId: string, householdId: string, options?: {
    storeId?: string;
    startDate?: Date;
    endDate?: Date;
    limit?: number;
  }): Promise<MedicinePriceRecord[]>;

  /** Compare current prices across stores for a medicine */
  compareStores(medicineId: string, householdId: string): Promise<StoreComparison[]>;

  /** Estimate price based on most recent record */
  estimatePrice(medicineId: string, householdId: string, storeId?: string): Promise<number | null>;

  /** Spending analytics over time */
  getAnalytics(householdId: string, period: 'month' | 'quarter' | 'year'): Promise<MedicineSpendingAnalytics>;
}

4.4 — Refill Alert Service

class RefillAlertService {
  /**
   * For each medicine in the user's active regimens:
   * 1. Get burn rate from BurnRateService (Phase 3)
   * 2. If daysUntilEmpty <= thresholdDays, create alert
   * 3. Attach last known price + cheapest store option
   * 4. Calculate suggested quantity (enough for configurable days, default 30)
   */
  getAlerts(householdId: string, userId: string, thresholdDays?: number): Promise<RefillAlert[]>;
}

4.5 — Refill Lists

  • CRUD for refill lists
  • POST /refills/lists with optional fromAlerts: true to auto-populate from current alerts
  • Each item can have an estimated price (from price history) and an actual price (entered when purchased)
  • Store comparison: for each item, find cheapest store based on recent price records

4.6 — Refill to Cabinet Flow

  • POST /refills/lists/:id/add-to-cabinet:
    • For each checked item with addedToCabinet: false:
      • Create a CabinetItem (status: active, purchaseDate: today)
      • If actualPrice was entered, create a MedicinePriceRecord
      • Mark addedToCabinet: true
    • Return summary: { addedCount, priceRecordsCreated }

4.7 — Price Analytics

interface MedicineSpendingAnalytics {
  /** Total spending per period */
  spendingOverTime: { period: string; total: number }[];

  /** Most expensive medicines */
  topBySpending: {
    medicineId: string;
    medicineName: string;
    totalSpent: number;
    avgPricePerUnit: number;
  }[];

  /** Per-store spending */
  spendingByStore: {
    storeId: string;
    storeName: string;
    totalSpent: number;
    purchaseCount: number;
  }[];

  /** Price trend alerts (significant increases) */
  priceAlerts: {
    medicineId: string;
    medicineName: string;
    storeName: string;
    previousPrice: number;
    currentPrice: number;
    changePercent: number;
  }[];
}

4.8 — Web UI: Pharmacies & Prices

  • /stores page:
    • Store list with CRUD
    • Filter by tags (pharmacy, grocery, etc.)
    • Per-store summary: total spent, last visit, item count
  • /medicine-prices page:
    • Medicine search -> price history line chart (per store, color-coded)
    • Store comparison table for selected medicine
    • Spending over time bar chart
    • Price alert panel

4.9 — Web UI: Refill Management

  • /refills page:
    • Alerts section: medicines running low
      • Card per medicine: name, days remaining, suggested quantity, cheapest store
      • "Generate Refill List" button -> creates list from all alerts
    • Refill lists section:
      • Active lists at top, completed/archived below
      • List detail view:
        • Items with checkbox, medicine name, quantity, estimated price
        • Check off: optionally enter actual price
        • Store comparison panel
        • "Done Shopping" -> prompts "Add items to cabinet?"
  • Dashboard widget: refill alert count badge, medicines needing refill soon

Acceptance Criteria

  • Can create and manage stores with tags
  • Can record medicine prices and view price history
  • Store comparison shows cheapest option per medicine
  • Refill alerts correctly identify medicines running low based on burn rate
  • Refill lists can be auto-generated from alerts
  • Checked refill items can be added to cabinet in one action
  • Price analytics show spending trends
  • Store infrastructure is reusable for food tracking (Phase 9)
  • All queries scoped to householdId

Estimated Effort

Medium-large. Store/price infrastructure, refill alert logic, and the refill-to-cabinet flow involve significant work. The store comparison and analytics add moderate complexity.