Implement regimens
This commit is contained in:
parent
1f66fab30f
commit
9f416903ef
66 changed files with 9130 additions and 189 deletions
39
packages/shared/src/enums/cabinet-event.enums.test.ts
Normal file
39
packages/shared/src/enums/cabinet-event.enums.test.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
CabinetEventType,
|
||||
CabinetEventSourceType,
|
||||
} from './cabinet-event.enums.js';
|
||||
|
||||
describe(CabinetEventType.name, () => {
|
||||
it('has exactly 6 values', () => {
|
||||
expect(Object.values(CabinetEventType)).toHaveLength(6);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['PURCHASED', 'purchased'],
|
||||
['CONSUMED', 'consumed'],
|
||||
['ADJUSTED', 'adjusted'],
|
||||
['DISCARDED', 'discarded'],
|
||||
['RESTORED', 'restored'],
|
||||
['DELETED', 'deleted'],
|
||||
])('%s = %s', (key, value) => {
|
||||
expect(CabinetEventType[key as keyof typeof CabinetEventType]).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe(CabinetEventSourceType.name, () => {
|
||||
it('has exactly 4 values', () => {
|
||||
expect(Object.values(CabinetEventSourceType)).toHaveLength(4);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['MANUAL', 'manual'],
|
||||
['ORGANIZER_FILL', 'organizer_fill'],
|
||||
['ORGANIZER_UNDO', 'organizer_undo'],
|
||||
['REFILL_LIST', 'refill_list'],
|
||||
])('%s = %s', (key, value) => {
|
||||
expect(
|
||||
CabinetEventSourceType[key as keyof typeof CabinetEventSourceType],
|
||||
).toBe(value);
|
||||
});
|
||||
});
|
||||
15
packages/shared/src/enums/cabinet-event.enums.ts
Normal file
15
packages/shared/src/enums/cabinet-event.enums.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export enum CabinetEventType {
|
||||
PURCHASED = 'purchased',
|
||||
CONSUMED = 'consumed',
|
||||
ADJUSTED = 'adjusted',
|
||||
DISCARDED = 'discarded',
|
||||
RESTORED = 'restored',
|
||||
DELETED = 'deleted',
|
||||
}
|
||||
|
||||
export enum CabinetEventSourceType {
|
||||
MANUAL = 'manual',
|
||||
ORGANIZER_FILL = 'organizer_fill',
|
||||
ORGANIZER_UNDO = 'organizer_undo',
|
||||
REFILL_LIST = 'refill_list',
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
export * from './roles.enums.js';
|
||||
export * from './medicine.enums.js';
|
||||
export * from './cabinet.enums.js';
|
||||
export * from './cabinet-event.enums.js';
|
||||
export * from './regimen.enums.js';
|
||||
|
|
|
|||
55
packages/shared/src/enums/regimen.enums.test.ts
Normal file
55
packages/shared/src/enums/regimen.enums.test.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
DosageFrequency,
|
||||
TimeOfDay,
|
||||
OrganizerFillStatus,
|
||||
} from './regimen.enums.js';
|
||||
|
||||
describe(DosageFrequency.name, () => {
|
||||
it('has exactly 7 values', () => {
|
||||
expect(Object.values(DosageFrequency)).toHaveLength(7);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['DAILY', 'daily'],
|
||||
['TWICE_DAILY', 'twice_daily'],
|
||||
['THREE_TIMES_DAILY', 'three_times_daily'],
|
||||
['WEEKLY', 'weekly'],
|
||||
['EVERY_OTHER_DAY', 'every_other_day'],
|
||||
['AS_NEEDED', 'as_needed'],
|
||||
['CUSTOM', 'custom'],
|
||||
])('%s = %s', (key, value) => {
|
||||
expect(DosageFrequency[key as keyof typeof DosageFrequency]).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe(TimeOfDay.name, () => {
|
||||
it('has exactly 4 values', () => {
|
||||
expect(Object.values(TimeOfDay)).toHaveLength(4);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['MORNING', 'morning'],
|
||||
['AFTERNOON', 'afternoon'],
|
||||
['EVENING', 'evening'],
|
||||
['BEDTIME', 'bedtime'],
|
||||
])('%s = %s', (key, value) => {
|
||||
expect(TimeOfDay[key as keyof typeof TimeOfDay]).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe(OrganizerFillStatus.name, () => {
|
||||
it('has exactly 3 values', () => {
|
||||
expect(Object.values(OrganizerFillStatus)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['COMPLETED', 'completed'],
|
||||
['PARTIAL', 'partial'],
|
||||
['REVERSED', 'reversed'],
|
||||
])('%s = %s', (key, value) => {
|
||||
expect(
|
||||
OrganizerFillStatus[key as keyof typeof OrganizerFillStatus],
|
||||
).toBe(value);
|
||||
});
|
||||
});
|
||||
22
packages/shared/src/enums/regimen.enums.ts
Normal file
22
packages/shared/src/enums/regimen.enums.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export enum DosageFrequency {
|
||||
DAILY = 'daily',
|
||||
TWICE_DAILY = 'twice_daily',
|
||||
THREE_TIMES_DAILY = 'three_times_daily',
|
||||
WEEKLY = 'weekly',
|
||||
EVERY_OTHER_DAY = 'every_other_day',
|
||||
AS_NEEDED = 'as_needed',
|
||||
CUSTOM = 'custom',
|
||||
}
|
||||
|
||||
export enum TimeOfDay {
|
||||
MORNING = 'morning',
|
||||
AFTERNOON = 'afternoon',
|
||||
EVENING = 'evening',
|
||||
BEDTIME = 'bedtime',
|
||||
}
|
||||
|
||||
export enum OrganizerFillStatus {
|
||||
COMPLETED = 'completed',
|
||||
PARTIAL = 'partial',
|
||||
REVERSED = 'reversed',
|
||||
}
|
||||
|
|
@ -6,3 +6,6 @@ export * from './enums/index.js';
|
|||
|
||||
// Validation schemas
|
||||
export * from './validation/index.js';
|
||||
|
||||
// Utils
|
||||
export * from './utils/index.js';
|
||||
|
|
|
|||
13
packages/shared/src/types/burn-rate.ts
Normal file
13
packages/shared/src/types/burn-rate.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export interface BurnRate {
|
||||
medicineId: string;
|
||||
medicineName: string;
|
||||
dailyConsumption: number;
|
||||
totalInCabinet: number;
|
||||
daysUntilEmpty: number | null;
|
||||
earliestExpiry: Date | null;
|
||||
avgUnitPrice: number | null;
|
||||
projectedDailyCost: number | null;
|
||||
projectedMonthlyCost: number | null;
|
||||
projectedYearlyCost: number | null;
|
||||
currency: string | null;
|
||||
}
|
||||
48
packages/shared/src/types/cabinet-event.ts
Normal file
48
packages/shared/src/types/cabinet-event.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import type {
|
||||
CabinetEventType,
|
||||
CabinetEventSourceType,
|
||||
} from '../enums/cabinet-event.enums.js';
|
||||
|
||||
export interface CabinetEvent {
|
||||
id: string;
|
||||
householdId: string;
|
||||
userId: string;
|
||||
cabinetItemId: string;
|
||||
medicineId: string;
|
||||
medicineName: string;
|
||||
eventType: CabinetEventType;
|
||||
quantity: number;
|
||||
quantityBefore: number;
|
||||
quantityAfter: number;
|
||||
unitPrice?: number;
|
||||
totalPrice?: number;
|
||||
currency?: string;
|
||||
storeId?: string;
|
||||
storeName?: string;
|
||||
sourceType: CabinetEventSourceType;
|
||||
sourceId?: string;
|
||||
reason?: string;
|
||||
notes?: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface SpendingSummary {
|
||||
totalSpent: number;
|
||||
currency: string | null;
|
||||
byMedicine: SpendingByMedicine[];
|
||||
byPeriod: SpendingByPeriod[];
|
||||
}
|
||||
|
||||
export interface SpendingByMedicine {
|
||||
medicineId: string;
|
||||
medicineName: string;
|
||||
totalSpent: number;
|
||||
totalQuantity: number;
|
||||
avgUnitPrice: number;
|
||||
purchaseCount: number;
|
||||
}
|
||||
|
||||
export interface SpendingByPeriod {
|
||||
period: string;
|
||||
totalSpent: number;
|
||||
}
|
||||
|
|
@ -22,6 +22,12 @@ export interface CabinetItem {
|
|||
unit: DosageUnit;
|
||||
expirationDate?: Date;
|
||||
status: CabinetItemStatus;
|
||||
purchaseDate?: Date;
|
||||
unitPrice?: number;
|
||||
totalPrice?: number;
|
||||
currency?: string;
|
||||
storeId?: string;
|
||||
storeName?: string;
|
||||
notes?: string;
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
|
|
|
|||
|
|
@ -3,3 +3,7 @@ export * from './household.js';
|
|||
export * from './common.js';
|
||||
export * from './medicine.js';
|
||||
export * from './cabinet.js';
|
||||
export * from './cabinet-event.js';
|
||||
export * from './regimen.js';
|
||||
export * from './organizer-fill.js';
|
||||
export * from './burn-rate.js';
|
||||
|
|
|
|||
31
packages/shared/src/types/organizer-fill.ts
Normal file
31
packages/shared/src/types/organizer-fill.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { OrganizerFillStatus } from '../enums/regimen.enums.js';
|
||||
|
||||
export interface OrganizerFill {
|
||||
id: string;
|
||||
householdId: string;
|
||||
userId: string;
|
||||
regimenId: string;
|
||||
regimenName: string;
|
||||
numberOfDays: number;
|
||||
fillDate: Date;
|
||||
items: OrganizerFillItem[];
|
||||
status: OrganizerFillStatus;
|
||||
notes?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface OrganizerFillItem {
|
||||
medicineId: string;
|
||||
medicineName: string;
|
||||
quantityNeeded: number;
|
||||
quantityTaken: number;
|
||||
wasShort: boolean;
|
||||
shortage: number;
|
||||
deductions: OrganizerDeduction[];
|
||||
}
|
||||
|
||||
export interface OrganizerDeduction {
|
||||
cabinetItemId: string;
|
||||
quantityTaken: number;
|
||||
}
|
||||
31
packages/shared/src/types/regimen.ts
Normal file
31
packages/shared/src/types/regimen.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type {
|
||||
DosageFrequency,
|
||||
TimeOfDay,
|
||||
} from '../enums/regimen.enums.js';
|
||||
import type { DosageUnit, MedicineForm, StrengthUnit } from '../enums/medicine.enums.js';
|
||||
|
||||
export interface Regimen {
|
||||
id: string;
|
||||
householdId: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
medications: RegimenMedication[];
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface RegimenMedication {
|
||||
medicineId: string;
|
||||
medicineName: string;
|
||||
medicineStrength: number;
|
||||
medicineStrengthUnit: StrengthUnit;
|
||||
medicineForm: MedicineForm;
|
||||
dosage: number;
|
||||
dosageUnit: DosageUnit;
|
||||
frequency: DosageFrequency;
|
||||
customFrequencyPerDay?: number;
|
||||
timeOfDay?: TimeOfDay;
|
||||
instructions?: string;
|
||||
}
|
||||
68
packages/shared/src/utils/frequency.test.ts
Normal file
68
packages/shared/src/utils/frequency.test.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { DosageFrequency } from '../enums/regimen.enums.js';
|
||||
import { getFrequencyMultiplier, calculateQuantityNeeded } from './frequency.js';
|
||||
|
||||
describe('getFrequencyMultiplier', () => {
|
||||
it.each([
|
||||
[DosageFrequency.DAILY, undefined, 1],
|
||||
[DosageFrequency.TWICE_DAILY, undefined, 2],
|
||||
[DosageFrequency.THREE_TIMES_DAILY, undefined, 3],
|
||||
[DosageFrequency.WEEKLY, undefined, 1 / 7],
|
||||
[DosageFrequency.EVERY_OTHER_DAY, undefined, 1 / 2],
|
||||
[DosageFrequency.AS_NEEDED, undefined, 0],
|
||||
[DosageFrequency.CUSTOM, 4, 4],
|
||||
[DosageFrequency.CUSTOM, undefined, 0],
|
||||
])('%s (customPerDay=%s) = %s', (frequency, custom, expected) => {
|
||||
expect(getFrequencyMultiplier(frequency, custom)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateQuantityNeeded', () => {
|
||||
it('daily: 2 pills * 7 days = 14', () => {
|
||||
expect(calculateQuantityNeeded(2, DosageFrequency.DAILY, 7)).toBe(14);
|
||||
});
|
||||
|
||||
it('twice_daily: 1 pill * 2 * 7 days = 14', () => {
|
||||
expect(calculateQuantityNeeded(1, DosageFrequency.TWICE_DAILY, 7)).toBe(14);
|
||||
});
|
||||
|
||||
it('three_times_daily: 1 pill * 3 * 7 days = 21', () => {
|
||||
expect(calculateQuantityNeeded(1, DosageFrequency.THREE_TIMES_DAILY, 7)).toBe(21);
|
||||
});
|
||||
|
||||
it('weekly: 1 pill * ceil(7/7) = 1', () => {
|
||||
expect(calculateQuantityNeeded(1, DosageFrequency.WEEKLY, 7)).toBe(1);
|
||||
});
|
||||
|
||||
it('weekly: 2 pills * ceil(10/7) = 4', () => {
|
||||
expect(calculateQuantityNeeded(2, DosageFrequency.WEEKLY, 10)).toBe(4);
|
||||
});
|
||||
|
||||
it('every_other_day: 1 pill * ceil(7/2) = 4', () => {
|
||||
expect(calculateQuantityNeeded(1, DosageFrequency.EVERY_OTHER_DAY, 7)).toBe(4);
|
||||
});
|
||||
|
||||
it('every_other_day: 1 pill * ceil(6/2) = 3', () => {
|
||||
expect(calculateQuantityNeeded(1, DosageFrequency.EVERY_OTHER_DAY, 6)).toBe(3);
|
||||
});
|
||||
|
||||
it('as_needed: always 0', () => {
|
||||
expect(calculateQuantityNeeded(5, DosageFrequency.AS_NEEDED, 30)).toBe(0);
|
||||
});
|
||||
|
||||
it('custom: 1 pill * 4/day * 7 days = 28', () => {
|
||||
expect(calculateQuantityNeeded(1, DosageFrequency.CUSTOM, 7, 4)).toBe(28);
|
||||
});
|
||||
|
||||
it('custom without customFrequencyPerDay: 0', () => {
|
||||
expect(calculateQuantityNeeded(1, DosageFrequency.CUSTOM, 7)).toBe(0);
|
||||
});
|
||||
|
||||
it('handles fractional dosage: 0.5 pill * 7 days = 3.5', () => {
|
||||
expect(calculateQuantityNeeded(0.5, DosageFrequency.DAILY, 7)).toBe(3.5);
|
||||
});
|
||||
|
||||
it('single day daily: 1 pill', () => {
|
||||
expect(calculateQuantityNeeded(1, DosageFrequency.DAILY, 1)).toBe(1);
|
||||
});
|
||||
});
|
||||
63
packages/shared/src/utils/frequency.ts
Normal file
63
packages/shared/src/utils/frequency.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { DosageFrequency } from '../enums/regimen.enums.js';
|
||||
|
||||
/**
|
||||
* Returns the daily frequency multiplier for a given dosage frequency.
|
||||
* For example, TWICE_DAILY returns 2, WEEKLY returns 1/7.
|
||||
* AS_NEEDED returns 0 (excluded from calculations).
|
||||
*/
|
||||
export function getFrequencyMultiplier(
|
||||
frequency: DosageFrequency,
|
||||
customPerDay?: number,
|
||||
): number {
|
||||
switch (frequency) {
|
||||
case DosageFrequency.DAILY:
|
||||
return 1;
|
||||
case DosageFrequency.TWICE_DAILY:
|
||||
return 2;
|
||||
case DosageFrequency.THREE_TIMES_DAILY:
|
||||
return 3;
|
||||
case DosageFrequency.WEEKLY:
|
||||
return 1 / 7;
|
||||
case DosageFrequency.EVERY_OTHER_DAY:
|
||||
return 1 / 2;
|
||||
case DosageFrequency.AS_NEEDED:
|
||||
return 0;
|
||||
case DosageFrequency.CUSTOM:
|
||||
return customPerDay ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total quantity needed for N days based on dosage and frequency.
|
||||
*
|
||||
* daily: dosage * numberOfDays
|
||||
* twice_daily: dosage * 2 * numberOfDays
|
||||
* three_times_daily: dosage * 3 * numberOfDays
|
||||
* weekly: dosage * ceil(numberOfDays / 7)
|
||||
* every_other_day: dosage * ceil(numberOfDays / 2)
|
||||
* as_needed: 0 (excluded from organizer fills)
|
||||
* custom: dosage * customFrequencyPerDay * numberOfDays
|
||||
*/
|
||||
export function calculateQuantityNeeded(
|
||||
dosage: number,
|
||||
frequency: DosageFrequency,
|
||||
numberOfDays: number,
|
||||
customFrequencyPerDay?: number,
|
||||
): number {
|
||||
switch (frequency) {
|
||||
case DosageFrequency.DAILY:
|
||||
return dosage * numberOfDays;
|
||||
case DosageFrequency.TWICE_DAILY:
|
||||
return dosage * 2 * numberOfDays;
|
||||
case DosageFrequency.THREE_TIMES_DAILY:
|
||||
return dosage * 3 * numberOfDays;
|
||||
case DosageFrequency.WEEKLY:
|
||||
return dosage * Math.ceil(numberOfDays / 7);
|
||||
case DosageFrequency.EVERY_OTHER_DAY:
|
||||
return dosage * Math.ceil(numberOfDays / 2);
|
||||
case DosageFrequency.AS_NEEDED:
|
||||
return 0;
|
||||
case DosageFrequency.CUSTOM:
|
||||
return dosage * (customFrequencyPerDay ?? 0) * numberOfDays;
|
||||
}
|
||||
}
|
||||
1
packages/shared/src/utils/index.ts
Normal file
1
packages/shared/src/utils/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './frequency.js';
|
||||
111
packages/shared/src/validation/cabinet-event.schemas.test.ts
Normal file
111
packages/shared/src/validation/cabinet-event.schemas.test.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { CabinetEventType } from '../enums/cabinet-event.enums.js';
|
||||
import {
|
||||
CabinetEventQuerySchema,
|
||||
SpendingSummaryQuerySchema,
|
||||
DiscardCabinetItemSchema,
|
||||
} from './cabinet-event.schemas.js';
|
||||
|
||||
describe('CabinetEventQuerySchema', () => {
|
||||
it('applies default limit', () => {
|
||||
const result = CabinetEventQuerySchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.limit).toBe(20);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts all filters', () => {
|
||||
const result = CabinetEventQuerySchema.safeParse({
|
||||
medicineId: 'med-1',
|
||||
eventType: CabinetEventType.PURCHASED,
|
||||
startDate: '2026-01-01T00:00:00.000Z',
|
||||
endDate: '2026-12-31T00:00:00.000Z',
|
||||
cursor: 'abc',
|
||||
limit: 50,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid eventType', () => {
|
||||
const result = CabinetEventQuerySchema.safeParse({ eventType: 'invalid' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects limit over 100', () => {
|
||||
const result = CabinetEventQuerySchema.safeParse({ limit: 101 });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SpendingSummaryQuerySchema', () => {
|
||||
it('defaults period to month', () => {
|
||||
const result = SpendingSummaryQuerySchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.period).toBe('month');
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts quarter period', () => {
|
||||
const result = SpendingSummaryQuerySchema.safeParse({ period: 'quarter' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts year period', () => {
|
||||
const result = SpendingSummaryQuerySchema.safeParse({ period: 'year' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid period', () => {
|
||||
const result = SpendingSummaryQuerySchema.safeParse({ period: 'week' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts optional medicineId and date range', () => {
|
||||
const result = SpendingSummaryQuerySchema.safeParse({
|
||||
medicineId: 'med-1',
|
||||
startDate: '2026-01-01T00:00:00.000Z',
|
||||
endDate: '2026-06-30T00:00:00.000Z',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DiscardCabinetItemSchema', () => {
|
||||
it('accepts valid input', () => {
|
||||
const result = DiscardCabinetItemSchema.safeParse({
|
||||
reason: 'expired',
|
||||
notes: 'Found during cleanup',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts reason only', () => {
|
||||
const result = DiscardCabinetItemSchema.safeParse({ reason: 'damaged' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects empty reason', () => {
|
||||
const result = DiscardCabinetItemSchema.safeParse({ reason: '' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing reason', () => {
|
||||
const result = DiscardCabinetItemSchema.safeParse({});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('trims reason', () => {
|
||||
const result = DiscardCabinetItemSchema.safeParse({ reason: ' expired ' });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.reason).toBe('expired');
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects reason over 500 chars', () => {
|
||||
const result = DiscardCabinetItemSchema.safeParse({ reason: 'x'.repeat(501) });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
87
packages/shared/src/validation/cabinet-event.schemas.ts
Normal file
87
packages/shared/src/validation/cabinet-event.schemas.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { z } from 'zod/v4';
|
||||
import { CabinetEventType } from '../enums/cabinet-event.enums.js';
|
||||
|
||||
// --- Query ---
|
||||
|
||||
export const CabinetEventQuerySchema = z.object({
|
||||
medicineId: z.string().optional(),
|
||||
eventType: z.nativeEnum(CabinetEventType).optional(),
|
||||
startDate: z.iso.datetime().optional(),
|
||||
endDate: z.iso.datetime().optional(),
|
||||
cursor: z.string().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||
});
|
||||
|
||||
export const SpendingSummaryQuerySchema = z.object({
|
||||
period: z.enum(['month', 'quarter', 'year']).default('month'),
|
||||
medicineId: z.string().optional(),
|
||||
startDate: z.iso.datetime().optional(),
|
||||
endDate: z.iso.datetime().optional(),
|
||||
});
|
||||
|
||||
// --- Discard ---
|
||||
|
||||
export const DiscardCabinetItemSchema = z.object({
|
||||
reason: z.string().min(1).max(500).trim(),
|
||||
notes: z.string().max(1000).trim().optional(),
|
||||
});
|
||||
|
||||
// --- Response ---
|
||||
|
||||
export const CabinetEventResponseSchema = z.object({
|
||||
_id: z.string(),
|
||||
householdId: z.string(),
|
||||
userId: z.string(),
|
||||
cabinetItemId: z.string(),
|
||||
medicineId: z.string(),
|
||||
medicineName: z.string(),
|
||||
eventType: z.string(),
|
||||
quantity: z.number(),
|
||||
quantityBefore: z.number(),
|
||||
quantityAfter: z.number(),
|
||||
unitPrice: z.number().optional(),
|
||||
totalPrice: z.number().optional(),
|
||||
currency: z.string().optional(),
|
||||
storeId: z.string().optional(),
|
||||
storeName: z.string().optional(),
|
||||
sourceType: z.string(),
|
||||
sourceId: z.string().optional(),
|
||||
reason: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
createdAt: z.string(),
|
||||
});
|
||||
|
||||
export const CabinetEventListResponseSchema = z.object({
|
||||
data: z.array(CabinetEventResponseSchema),
|
||||
pagination: z.object({
|
||||
cursor: z.string().nullable(),
|
||||
hasMore: z.boolean(),
|
||||
total: z.number().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const SpendingByMedicineSchema = z.object({
|
||||
medicineId: z.string(),
|
||||
medicineName: z.string(),
|
||||
totalSpent: z.number(),
|
||||
totalQuantity: z.number(),
|
||||
avgUnitPrice: z.number(),
|
||||
purchaseCount: z.number(),
|
||||
});
|
||||
|
||||
export const SpendingByPeriodSchema = z.object({
|
||||
period: z.string(),
|
||||
totalSpent: z.number(),
|
||||
});
|
||||
|
||||
export const SpendingSummaryResponseSchema = z.object({
|
||||
totalSpent: z.number(),
|
||||
currency: z.string().nullable(),
|
||||
byMedicine: z.array(SpendingByMedicineSchema),
|
||||
byPeriod: z.array(SpendingByPeriodSchema),
|
||||
});
|
||||
|
||||
// Type exports
|
||||
export type CabinetEventQueryInput = z.infer<typeof CabinetEventQuerySchema>;
|
||||
export type SpendingSummaryQueryInput = z.infer<typeof SpendingSummaryQuerySchema>;
|
||||
export type DiscardCabinetItemInput = z.infer<typeof DiscardCabinetItemSchema>;
|
||||
|
|
@ -10,6 +10,12 @@ export const CreateCabinetItemSchema = z.object({
|
|||
quantity: z.number().nonnegative(),
|
||||
unit: z.nativeEnum(DosageUnit),
|
||||
expirationDate: z.iso.datetime().optional(),
|
||||
purchaseDate: z.iso.datetime().optional(),
|
||||
unitPrice: z.number().nonnegative().optional(),
|
||||
totalPrice: z.number().nonnegative().optional(),
|
||||
currency: z.string().max(10).trim().optional(),
|
||||
storeId: z.string().min(1).optional(),
|
||||
storeName: z.string().max(200).trim().optional(),
|
||||
notes: z.string().max(1000).trim().optional(),
|
||||
});
|
||||
|
||||
|
|
@ -50,6 +56,12 @@ export const CabinetItemResponseSchema = z.object({
|
|||
unit: z.string(),
|
||||
expirationDate: z.string().optional(),
|
||||
status: z.string(),
|
||||
purchaseDate: z.string().optional(),
|
||||
unitPrice: z.number().optional(),
|
||||
totalPrice: z.number().optional(),
|
||||
currency: z.string().optional(),
|
||||
storeId: z.string().optional(),
|
||||
storeName: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
createdBy: z.string(),
|
||||
createdAt: z.string(),
|
||||
|
|
|
|||
|
|
@ -2,3 +2,6 @@ export * from './user.schemas.js';
|
|||
export * from './household.schemas.js';
|
||||
export * from './medicine.schemas.js';
|
||||
export * from './cabinet.schemas.js';
|
||||
export * from './cabinet-event.schemas.js';
|
||||
export * from './regimen.schemas.js';
|
||||
export * from './organizer.schemas.js';
|
||||
|
|
|
|||
113
packages/shared/src/validation/organizer.schemas.test.ts
Normal file
113
packages/shared/src/validation/organizer.schemas.test.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { OrganizerFillStatus } from '../enums/regimen.enums.js';
|
||||
import {
|
||||
OrganizerPreviewSchema,
|
||||
OrganizerFillSchema,
|
||||
OrganizerFillQuerySchema,
|
||||
} from './organizer.schemas.js';
|
||||
|
||||
describe('OrganizerPreviewSchema', () => {
|
||||
it('accepts valid input', () => {
|
||||
const result = OrganizerPreviewSchema.safeParse({
|
||||
regimenId: 'reg-1',
|
||||
numberOfDays: 7,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('defaults numberOfDays to 7', () => {
|
||||
const result = OrganizerPreviewSchema.safeParse({ regimenId: 'reg-1' });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.numberOfDays).toBe(7);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects missing regimenId', () => {
|
||||
const result = OrganizerPreviewSchema.safeParse({ numberOfDays: 7 });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects numberOfDays over 90', () => {
|
||||
const result = OrganizerPreviewSchema.safeParse({
|
||||
regimenId: 'reg-1',
|
||||
numberOfDays: 91,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects numberOfDays of 0', () => {
|
||||
const result = OrganizerPreviewSchema.safeParse({
|
||||
regimenId: 'reg-1',
|
||||
numberOfDays: 0,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OrganizerFillSchema', () => {
|
||||
it('accepts valid input', () => {
|
||||
const result = OrganizerFillSchema.safeParse({
|
||||
regimenId: 'reg-1',
|
||||
numberOfDays: 7,
|
||||
allowPartial: true,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('defaults allowPartial to false', () => {
|
||||
const result = OrganizerFillSchema.safeParse({ regimenId: 'reg-1' });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.allowPartial).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts optional notes', () => {
|
||||
const result = OrganizerFillSchema.safeParse({
|
||||
regimenId: 'reg-1',
|
||||
notes: 'Weekly fill',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects notes over 1000 chars', () => {
|
||||
const result = OrganizerFillSchema.safeParse({
|
||||
regimenId: 'reg-1',
|
||||
notes: 'x'.repeat(1001),
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OrganizerFillQuerySchema', () => {
|
||||
it('applies default limit', () => {
|
||||
const result = OrganizerFillQuerySchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.limit).toBe(20);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts status filter', () => {
|
||||
const result = OrganizerFillQuerySchema.safeParse({
|
||||
status: OrganizerFillStatus.COMPLETED,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts regimenId filter', () => {
|
||||
const result = OrganizerFillQuerySchema.safeParse({ regimenId: 'reg-1' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid status', () => {
|
||||
const result = OrganizerFillQuerySchema.safeParse({ status: 'invalid' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects limit over 100', () => {
|
||||
const result = OrganizerFillQuerySchema.safeParse({ limit: 101 });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
93
packages/shared/src/validation/organizer.schemas.ts
Normal file
93
packages/shared/src/validation/organizer.schemas.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { z } from 'zod/v4';
|
||||
import { OrganizerFillStatus } from '../enums/regimen.enums.js';
|
||||
|
||||
// --- Request ---
|
||||
|
||||
export const OrganizerPreviewSchema = z.object({
|
||||
regimenId: z.string().min(1),
|
||||
numberOfDays: z.number().int().min(1).max(90).default(7),
|
||||
});
|
||||
|
||||
export const OrganizerFillSchema = z.object({
|
||||
regimenId: z.string().min(1),
|
||||
numberOfDays: z.number().int().min(1).max(90).default(7),
|
||||
allowPartial: z.boolean().default(false),
|
||||
notes: z.string().max(1000).trim().optional(),
|
||||
});
|
||||
|
||||
export const OrganizerFillQuerySchema = z.object({
|
||||
regimenId: z.string().optional(),
|
||||
status: z.nativeEnum(OrganizerFillStatus).optional(),
|
||||
cursor: z.string().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||
});
|
||||
|
||||
// --- Response ---
|
||||
|
||||
export const OrganizerDeductionResponseSchema = z.object({
|
||||
cabinetItemId: z.string(),
|
||||
quantityTaken: z.number(),
|
||||
});
|
||||
|
||||
export const OrganizerPreviewItemSchema = z.object({
|
||||
medicineId: z.string(),
|
||||
medicineName: z.string(),
|
||||
quantityNeeded: z.number(),
|
||||
quantityAvailable: z.number(),
|
||||
isShort: z.boolean(),
|
||||
shortage: z.number(),
|
||||
cabinetBreakdown: z.array(
|
||||
z.object({
|
||||
cabinetItemId: z.string(),
|
||||
expirationDate: z.string().nullable(),
|
||||
quantityToTake: z.number(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export const OrganizerPreviewResponseSchema = z.object({
|
||||
regimenName: z.string(),
|
||||
numberOfDays: z.number(),
|
||||
items: z.array(OrganizerPreviewItemSchema),
|
||||
canFillCompletely: z.boolean(),
|
||||
hasShortages: z.boolean(),
|
||||
});
|
||||
|
||||
export const OrganizerFillItemResponseSchema = z.object({
|
||||
medicineId: z.string(),
|
||||
medicineName: z.string(),
|
||||
quantityNeeded: z.number(),
|
||||
quantityTaken: z.number(),
|
||||
wasShort: z.boolean(),
|
||||
shortage: z.number(),
|
||||
deductions: z.array(OrganizerDeductionResponseSchema),
|
||||
});
|
||||
|
||||
export const OrganizerFillResponseSchema = z.object({
|
||||
_id: z.string(),
|
||||
householdId: z.string(),
|
||||
userId: z.string(),
|
||||
regimenId: z.string(),
|
||||
regimenName: z.string(),
|
||||
numberOfDays: z.number(),
|
||||
fillDate: z.string(),
|
||||
items: z.array(OrganizerFillItemResponseSchema),
|
||||
status: z.string(),
|
||||
notes: z.string().optional(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
|
||||
export const OrganizerFillListResponseSchema = z.object({
|
||||
data: z.array(OrganizerFillResponseSchema),
|
||||
pagination: z.object({
|
||||
cursor: z.string().nullable(),
|
||||
hasMore: z.boolean(),
|
||||
total: z.number().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Type exports
|
||||
export type OrganizerPreviewInput = z.infer<typeof OrganizerPreviewSchema>;
|
||||
export type OrganizerFillInput = z.infer<typeof OrganizerFillSchema>;
|
||||
export type OrganizerFillQueryInput = z.infer<typeof OrganizerFillQuerySchema>;
|
||||
157
packages/shared/src/validation/regimen.schemas.test.ts
Normal file
157
packages/shared/src/validation/regimen.schemas.test.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { DosageUnit } from '../enums/medicine.enums.js';
|
||||
import { DosageFrequency, TimeOfDay } from '../enums/regimen.enums.js';
|
||||
import {
|
||||
CreateRegimenSchema,
|
||||
UpdateRegimenSchema,
|
||||
RegimenQuerySchema,
|
||||
RegimenMedicationInputSchema,
|
||||
} from './regimen.schemas.js';
|
||||
|
||||
const validMedication = {
|
||||
medicineId: 'med-1',
|
||||
dosage: 1,
|
||||
dosageUnit: DosageUnit.TABLET,
|
||||
frequency: DosageFrequency.DAILY,
|
||||
};
|
||||
|
||||
describe('RegimenMedicationInputSchema', () => {
|
||||
it('accepts valid medication', () => {
|
||||
const result = RegimenMedicationInputSchema.safeParse(validMedication);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts full medication with optional fields', () => {
|
||||
const result = RegimenMedicationInputSchema.safeParse({
|
||||
...validMedication,
|
||||
timeOfDay: TimeOfDay.MORNING,
|
||||
instructions: 'Take with food',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts custom frequency with customFrequencyPerDay', () => {
|
||||
const result = RegimenMedicationInputSchema.safeParse({
|
||||
...validMedication,
|
||||
frequency: DosageFrequency.CUSTOM,
|
||||
customFrequencyPerDay: 4,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects custom frequency without customFrequencyPerDay', () => {
|
||||
const result = RegimenMedicationInputSchema.safeParse({
|
||||
...validMedication,
|
||||
frequency: DosageFrequency.CUSTOM,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects zero dosage', () => {
|
||||
const result = RegimenMedicationInputSchema.safeParse({
|
||||
...validMedication,
|
||||
dosage: 0,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing medicineId', () => {
|
||||
const { medicineId: _, ...rest } = validMedication;
|
||||
const result = RegimenMedicationInputSchema.safeParse(rest);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreateRegimenSchema', () => {
|
||||
const validRegimen = {
|
||||
name: 'Daily medications',
|
||||
medications: [validMedication],
|
||||
};
|
||||
|
||||
it('accepts valid input', () => {
|
||||
const result = CreateRegimenSchema.safeParse(validRegimen);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('defaults isActive to true', () => {
|
||||
const result = CreateRegimenSchema.safeParse(validRegimen);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.isActive).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts isActive override', () => {
|
||||
const result = CreateRegimenSchema.safeParse({ ...validRegimen, isActive: false });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.isActive).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects empty name', () => {
|
||||
const result = CreateRegimenSchema.safeParse({ ...validRegimen, name: '' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects empty medications array', () => {
|
||||
const result = CreateRegimenSchema.safeParse({ ...validRegimen, medications: [] });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts multiple medications', () => {
|
||||
const result = CreateRegimenSchema.safeParse({
|
||||
...validRegimen,
|
||||
medications: [
|
||||
validMedication,
|
||||
{ ...validMedication, medicineId: 'med-2', dosage: 2 },
|
||||
],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateRegimenSchema', () => {
|
||||
it('accepts partial update', () => {
|
||||
const result = UpdateRegimenSchema.safeParse({ name: 'Updated name' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts empty object', () => {
|
||||
const result = UpdateRegimenSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts isActive toggle', () => {
|
||||
const result = UpdateRegimenSchema.safeParse({ isActive: false });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects empty medications array when provided', () => {
|
||||
const result = UpdateRegimenSchema.safeParse({ medications: [] });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RegimenQuerySchema', () => {
|
||||
it('applies default limit', () => {
|
||||
const result = RegimenQuerySchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.limit).toBe(20);
|
||||
}
|
||||
});
|
||||
|
||||
it('parses isActive string to boolean', () => {
|
||||
const result = RegimenQuerySchema.safeParse({ isActive: 'true' });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.isActive).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects limit over 100', () => {
|
||||
const result = RegimenQuerySchema.safeParse({ limit: 101 });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
105
packages/shared/src/validation/regimen.schemas.ts
Normal file
105
packages/shared/src/validation/regimen.schemas.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { z } from 'zod/v4';
|
||||
import { DosageFrequency, TimeOfDay } from '../enums/regimen.enums.js';
|
||||
import { DosageUnit } from '../enums/medicine.enums.js';
|
||||
|
||||
// --- RegimenMedication (embedded) ---
|
||||
|
||||
export const RegimenMedicationInputSchema = z
|
||||
.object({
|
||||
medicineId: z.string().min(1),
|
||||
dosage: z.number().positive(),
|
||||
dosageUnit: z.nativeEnum(DosageUnit),
|
||||
frequency: z.nativeEnum(DosageFrequency),
|
||||
customFrequencyPerDay: z.number().int().positive().optional(),
|
||||
timeOfDay: z.nativeEnum(TimeOfDay).optional(),
|
||||
instructions: z.string().max(500).trim().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
data.frequency !== DosageFrequency.CUSTOM ||
|
||||
data.customFrequencyPerDay !== undefined,
|
||||
{ message: 'customFrequencyPerDay is required when frequency is custom' },
|
||||
);
|
||||
|
||||
// --- Regimen ---
|
||||
|
||||
export const CreateRegimenSchema = z.object({
|
||||
name: z.string().min(1).max(200).trim(),
|
||||
isActive: z.boolean().default(true),
|
||||
medications: z.array(RegimenMedicationInputSchema).min(1),
|
||||
});
|
||||
|
||||
export const UpdateRegimenSchema = z.object({
|
||||
name: z.string().min(1).max(200).trim().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
medications: z.array(RegimenMedicationInputSchema).min(1).optional(),
|
||||
});
|
||||
|
||||
export const RegimenQuerySchema = z.object({
|
||||
isActive: z
|
||||
.string()
|
||||
.transform((v) => v === 'true')
|
||||
.optional(),
|
||||
cursor: z.string().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||
});
|
||||
|
||||
// --- Response ---
|
||||
|
||||
export const RegimenMedicationResponseSchema = z.object({
|
||||
medicineId: z.string(),
|
||||
medicineName: z.string(),
|
||||
medicineStrength: z.number(),
|
||||
medicineStrengthUnit: z.string(),
|
||||
medicineForm: z.string(),
|
||||
dosage: z.number(),
|
||||
dosageUnit: z.string(),
|
||||
frequency: z.string(),
|
||||
customFrequencyPerDay: z.number().optional(),
|
||||
timeOfDay: z.string().optional(),
|
||||
instructions: z.string().optional(),
|
||||
});
|
||||
|
||||
export const RegimenResponseSchema = z.object({
|
||||
_id: z.string(),
|
||||
householdId: z.string(),
|
||||
userId: z.string(),
|
||||
name: z.string(),
|
||||
isActive: z.boolean(),
|
||||
medications: z.array(RegimenMedicationResponseSchema),
|
||||
createdBy: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
|
||||
export const RegimenListResponseSchema = z.object({
|
||||
data: z.array(RegimenResponseSchema),
|
||||
pagination: z.object({
|
||||
cursor: z.string().nullable(),
|
||||
hasMore: z.boolean(),
|
||||
total: z.number().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const BurnRateItemSchema = z.object({
|
||||
medicineId: z.string(),
|
||||
medicineName: z.string(),
|
||||
dailyConsumption: z.number(),
|
||||
totalInCabinet: z.number(),
|
||||
daysUntilEmpty: z.number().nullable(),
|
||||
earliestExpiry: z.string().nullable(),
|
||||
avgUnitPrice: z.number().nullable(),
|
||||
projectedDailyCost: z.number().nullable(),
|
||||
projectedMonthlyCost: z.number().nullable(),
|
||||
projectedYearlyCost: z.number().nullable(),
|
||||
currency: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const BurnRateResponseSchema = z.object({
|
||||
data: z.array(BurnRateItemSchema),
|
||||
});
|
||||
|
||||
// Type exports
|
||||
export type CreateRegimenInput = z.infer<typeof CreateRegimenSchema>;
|
||||
export type UpdateRegimenInput = z.infer<typeof UpdateRegimenSchema>;
|
||||
export type RegimenQueryInput = z.infer<typeof RegimenQuerySchema>;
|
||||
Loading…
Add table
Add a link
Reference in a new issue