7.9 KiB
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
CabinetItemMongoDB schema and full CRUD API- Aggregate quantity view per medicine
- Expiry date tracking and warnings
- Low stock alerts (based on configurable thresholds)
- 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:
CreateCabinetItemSchemaUpdateCabinetItemSchemaAdjustQuantitySchemaCabinetQuerySchema
2.2 — Mongoose Schema & Repository
packages/api/src/modules/cabinet/cabinet.repository.tsCabinetRepositorywith:findByHousehold(householdId, query)— filtered, paginatedfindById(id, householdId)findByMedicine(householdId, medicineId)— all items for a medicinegetAggregateSummary(householdId)— MongoDB aggregation pipelinecreate(data)update(id, householdId, data)adjustQuantity(id, householdId, delta)— atomic$incfindExpiringSoon(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):
- Query all active cabinet items with
expirationDate <= today - Update status to
expired - Create in-app notifications for expired items
- Query items expiring within 7 days, create warning notifications
- Query all active cabinet items with
2.5 — Web UI: Medicine Cabinet
/cabinetpage:- 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)
- Summary view (default): aggregated per medicine
/cabinet/alertsor 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
depletedwhen quantity reaches 0 - Items auto-transition to
expiredwhen 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).