UNPKG

emr-types

Version:

Comprehensive TypeScript Types Library for Electronic Medical Record (EMR) Applications - Domain-Driven Design with Zod Validation

615 lines 24.5 kB
import { z } from 'zod'; import { AppointmentStatus, AppointmentType } from '../domains/appointment'; // ============================================================================ // BASE SCHEMAS // ============================================================================ export const AppointmentIdSchema = z.string().uuid().brand(); export const TimeSlotSchema = z.object({ startTime: z.date(), endTime: z.date() }).refine((data) => data.startTime < data.endTime, { message: 'Start time must be before end time', path: ['endTime'] }); export const DurationSchema = z.object({ minutes: z.number().positive().max(1440), // max 24 hours hours: z.number().nonnegative(), days: z.number().nonnegative() }).refine((data) => data.minutes > 0 || data.hours > 0 || data.days > 0, { message: 'Duration must be greater than zero', path: ['minutes'] }); // ============================================================================ // ENUM SCHEMAS // ============================================================================ export const AppointmentStatusSchema = z.nativeEnum(AppointmentStatus); export const AppointmentTypeSchema = z.nativeEnum(AppointmentType); // ============================================================================ // VALUE OBJECT SCHEMAS // ============================================================================ export const AppointmentIdValueObjectSchema = z.object({ value: AppointmentIdSchema, format: z.enum(['uuid', 'auto-increment', 'custom']), createdAt: z.date(), metadata: z.record(z.unknown()).optional() }); export const TimeSlotValueObjectSchema = z.object({ value: TimeSlotSchema, isAvailable: z.boolean(), isBooked: z.boolean(), isBlocked: z.boolean(), blockReason: z.string().max(200).optional(), createdAt: z.date() }); export const DurationValueObjectSchema = z.object({ value: DurationSchema, isStandard: z.boolean(), isCustom: z.boolean(), createdAt: z.date() }); // ============================================================================ // ENTITY SCHEMAS // ============================================================================ export const AppointmentSlotSchema = z.object({ id: z.string().uuid(), appointmentId: AppointmentIdSchema.optional(), timeSlot: TimeSlotValueObjectSchema, duration: DurationValueObjectSchema, isAvailable: z.boolean(), isBooked: z.boolean(), isBlocked: z.boolean(), blockReason: z.string().max(200).optional(), recurringPattern: z.object({ frequency: z.enum(['daily', 'weekly', 'monthly', 'yearly']), interval: z.number().positive(), endDate: z.date().optional(), maxOccurrences: z.number().positive().optional(), daysOfWeek: z.array(z.number().min(0).max(6)).optional(), // 0 = Sunday dayOfMonth: z.number().min(1).max(31).optional(), monthOfYear: z.number().min(1).max(12).optional() }).optional(), createdAt: z.date(), updatedAt: z.date() }); export const AppointmentSchema = z.object({ id: AppointmentIdSchema, patientId: z.string().uuid(), providerId: z.string().uuid(), timeSlot: TimeSlotValueObjectSchema, duration: DurationValueObjectSchema, type: AppointmentTypeSchema, status: AppointmentStatusSchema, reason: z.string().min(1).max(500), notes: z.string().max(1000).optional(), symptoms: z.array(z.string().max(100)).optional(), diagnosis: z.array(z.object({ condition: z.string().min(1).max(100), icd10Code: z.string().max(20).optional(), severity: z.enum(['mild', 'moderate', 'severe']), notes: z.string().max(500).optional() })).optional(), prescriptions: z.array(z.object({ medication: z.string().min(1).max(100), dosage: z.string().max(50), frequency: z.string().max(50), duration: z.string().max(50), instructions: z.string().max(500).optional(), prescribedBy: z.string().uuid(), prescribedAt: z.date() })).optional(), procedures: z.array(z.object({ name: z.string().min(1).max(100), cptCode: z.string().max(20).optional(), description: z.string().max(500).optional(), performedBy: z.string().uuid(), performedAt: z.date().optional(), notes: z.string().max(500).optional() })).optional(), labOrders: z.array(z.object({ testName: z.string().min(1).max(100), loincCode: z.string().max(20).optional(), labName: z.string().max(100), orderedBy: z.string().uuid(), orderedAt: z.date(), results: z.array(z.object({ testName: z.string().min(1).max(100), value: z.string().max(100), unit: z.string().max(20).optional(), referenceRange: z.string().max(100).optional(), isAbnormal: z.boolean(), notes: z.string().max(500).optional(), reportedAt: z.date() })).optional() })).optional(), imagingOrders: z.array(z.object({ studyName: z.string().min(1).max(100), modality: z.enum(['x-ray', 'ct', 'mri', 'ultrasound', 'mammography', 'other']), bodyPart: z.string().max(100), facility: z.string().max(100), orderedBy: z.string().uuid(), orderedAt: z.date(), results: z.object({ report: z.string().max(1000), images: z.array(z.string().url()), radiologist: z.string().max(100).optional(), reportedAt: z.date() }).optional() })).optional(), referrals: z.array(z.object({ specialist: z.string().max(100), specialty: z.string().max(100), reason: z.string().max(500), urgency: z.enum(['routine', 'urgent', 'emergency']), referredBy: z.string().uuid(), referredAt: z.date(), notes: z.string().max(500).optional() })).optional(), followUp: z.object({ isRequired: z.boolean(), recommendedDate: z.date().optional(), type: AppointmentTypeSchema.optional(), reason: z.string().max(500).optional(), notes: z.string().max(500).optional() }).optional(), billing: z.object({ totalAmount: z.number().nonnegative(), insuranceAmount: z.number().nonnegative(), patientAmount: z.number().nonnegative(), copay: z.number().nonnegative(), deductible: z.number().nonnegative(), status: z.enum(['pending', 'billed', 'paid', 'denied', 'appealed']), invoiceNumber: z.string().max(50).optional(), billedAt: z.date().optional(), paidAt: z.date().optional() }).optional(), reminders: z.array(z.object({ type: z.enum(['email', 'sms', 'phone', 'push']), scheduledAt: z.date(), sentAt: z.date().optional(), status: z.enum(['scheduled', 'sent', 'failed', 'cancelled']), message: z.string().max(500).optional() })).optional(), attachments: z.array(z.object({ id: z.string().uuid(), name: z.string().min(1).max(100), type: z.enum(['document', 'image', 'video', 'audio']), url: z.string().url(), size: z.number().positive(), mimeType: z.string(), uploadedBy: z.string().uuid(), uploadedAt: z.date() })).optional(), tags: z.array(z.string().max(50)).optional(), createdAt: z.date(), updatedAt: z.date(), createdBy: z.string().uuid().optional(), updatedBy: z.string().uuid().optional(), cancelledAt: z.date().optional(), cancelledBy: z.string().uuid().optional(), cancellationReason: z.string().max(500).optional() }); // ============================================================================ // REQUEST/INPUT SCHEMAS // ============================================================================ export const CreateAppointmentRequestSchema = z.object({ patientId: z.string().uuid(), providerId: z.string().uuid(), startTime: z.date(), endTime: z.date(), type: AppointmentTypeSchema, reason: z.string().min(1).max(500), notes: z.string().max(1000).optional(), symptoms: z.array(z.string().max(100)).optional(), isRecurring: z.boolean().optional(), recurringPattern: z.object({ frequency: z.enum(['daily', 'weekly', 'monthly', 'yearly']), interval: z.number().positive(), endDate: z.date().optional(), maxOccurrences: z.number().positive().optional(), daysOfWeek: z.array(z.number().min(0).max(6)).optional(), dayOfMonth: z.number().min(1).max(31).optional(), monthOfYear: z.number().min(1).max(12).optional() }).optional(), reminders: z.array(z.object({ type: z.enum(['email', 'sms', 'phone', 'push']), minutesBefore: z.number().positive(), message: z.string().max(500).optional() })).optional(), tags: z.array(z.string().max(50)).optional() }).refine((data) => data.startTime < data.endTime, { message: 'Start time must be before end time', path: ['endTime'] }); export const UpdateAppointmentRequestSchema = z.object({ startTime: z.date().optional(), endTime: z.date().optional(), type: AppointmentTypeSchema.optional(), status: AppointmentStatusSchema.optional(), reason: z.string().min(1).max(500).optional(), notes: z.string().max(1000).optional(), symptoms: z.array(z.string().max(100)).optional(), diagnosis: z.array(z.object({ condition: z.string().min(1).max(100), icd10Code: z.string().max(20).optional(), severity: z.enum(['mild', 'moderate', 'severe']), notes: z.string().max(500).optional() })).optional(), prescriptions: z.array(z.object({ medication: z.string().min(1).max(100), dosage: z.string().max(50), frequency: z.string().max(50), duration: z.string().max(50), instructions: z.string().max(500).optional() })).optional(), procedures: z.array(z.object({ name: z.string().min(1).max(100), cptCode: z.string().max(20).optional(), description: z.string().max(500).optional(), notes: z.string().max(500).optional() })).optional(), labOrders: z.array(z.object({ testName: z.string().min(1).max(100), loincCode: z.string().max(20).optional(), labName: z.string().max(100) })).optional(), imagingOrders: z.array(z.object({ studyName: z.string().min(1).max(100), modality: z.enum(['x-ray', 'ct', 'mri', 'ultrasound', 'mammography', 'other']), bodyPart: z.string().max(100), facility: z.string().max(100) })).optional(), referrals: z.array(z.object({ specialist: z.string().max(100), specialty: z.string().max(100), reason: z.string().max(500), urgency: z.enum(['routine', 'urgent', 'emergency']), notes: z.string().max(500).optional() })).optional(), followUp: z.object({ isRequired: z.boolean(), recommendedDate: z.date().optional(), type: AppointmentTypeSchema.optional(), reason: z.string().max(500).optional(), notes: z.string().max(500).optional() }).optional(), tags: z.array(z.string().max(50)).optional() }); export const CancelAppointmentRequestSchema = z.object({ reason: z.string().min(1).max(500), sendNotification: z.boolean().optional() }); export const RescheduleAppointmentRequestSchema = z.object({ newStartTime: z.date(), newEndTime: z.date(), reason: z.string().max(500).optional(), sendNotification: z.boolean().optional() }).refine((data) => data.newStartTime < data.newEndTime, { message: 'New start time must be before new end time', path: ['newEndTime'] }); export const AddDiagnosisRequestSchema = z.object({ condition: z.string().min(1).max(100), icd10Code: z.string().max(20).optional(), severity: z.enum(['mild', 'moderate', 'severe']), notes: z.string().max(500).optional() }); export const AddPrescriptionRequestSchema = z.object({ medication: z.string().min(1).max(100), dosage: z.string().max(50), frequency: z.string().max(50), duration: z.string().max(50), instructions: z.string().max(500).optional() }); export const AddProcedureRequestSchema = z.object({ name: z.string().min(1).max(100), cptCode: z.string().max(20).optional(), description: z.string().max(500).optional(), notes: z.string().max(500).optional() }); export const AddLabOrderRequestSchema = z.object({ testName: z.string().min(1).max(100), loincCode: z.string().max(20).optional(), labName: z.string().max(100) }); export const AddImagingOrderRequestSchema = z.object({ studyName: z.string().min(1).max(100), modality: z.enum(['x-ray', 'ct', 'mri', 'ultrasound', 'mammography', 'other']), bodyPart: z.string().max(100), facility: z.string().max(100) }); export const AddReferralRequestSchema = z.object({ specialist: z.string().max(100), specialty: z.string().max(100), reason: z.string().max(500), urgency: z.enum(['routine', 'urgent', 'emergency']), notes: z.string().max(500).optional() }); export const ScheduleFollowUpRequestSchema = z.object({ recommendedDate: z.date(), type: AppointmentTypeSchema, reason: z.string().max(500), notes: z.string().max(500).optional() }); // ============================================================================ // RESPONSE SCHEMAS // ============================================================================ export const AppointmentResponseSchema = z.object({ id: AppointmentIdSchema, patientId: z.string().uuid(), providerId: z.string().uuid(), startTime: z.date(), endTime: z.date(), type: AppointmentTypeSchema, status: AppointmentStatusSchema, reason: z.string(), notes: z.string().optional(), symptoms: z.array(z.string()).optional(), diagnosis: z.array(z.object({ condition: z.string(), icd10Code: z.string().optional(), severity: z.enum(['mild', 'moderate', 'severe']), notes: z.string().optional() })).optional(), prescriptions: z.array(z.object({ medication: z.string(), dosage: z.string(), frequency: z.string(), duration: z.string(), instructions: z.string().optional() })).optional(), procedures: z.array(z.object({ name: z.string(), cptCode: z.string().optional(), description: z.string().optional(), performedAt: z.date().optional() })).optional(), labOrders: z.array(z.object({ testName: z.string(), loincCode: z.string().optional(), labName: z.string(), status: z.enum(['ordered', 'in-progress', 'completed', 'cancelled']) })).optional(), imagingOrders: z.array(z.object({ studyName: z.string(), modality: z.enum(['x-ray', 'ct', 'mri', 'ultrasound', 'mammography', 'other']), bodyPart: z.string(), facility: z.string(), status: z.enum(['ordered', 'in-progress', 'completed', 'cancelled']) })).optional(), referrals: z.array(z.object({ specialist: z.string(), specialty: z.string(), reason: z.string(), urgency: z.enum(['routine', 'urgent', 'emergency']), notes: z.string().optional() })).optional(), followUp: z.object({ isRequired: z.boolean(), recommendedDate: z.date().optional(), type: AppointmentTypeSchema.optional(), reason: z.string().optional() }).optional(), billing: z.object({ totalAmount: z.number().nonnegative(), insuranceAmount: z.number().nonnegative(), patientAmount: z.number().nonnegative(), status: z.enum(['pending', 'billed', 'paid', 'denied', 'appealed']) }).optional(), createdAt: z.date(), updatedAt: z.date(), cancelledAt: z.date().optional(), cancellationReason: z.string().optional() }); export const AppointmentListResponseSchema = z.object({ appointments: z.array(AppointmentResponseSchema), pagination: z.object({ page: z.number().positive(), limit: z.number().positive(), total: z.number().nonnegative(), totalPages: z.number().nonnegative() }) }); export const AppointmentSlotResponseSchema = z.object({ id: z.string().uuid(), startTime: z.date(), endTime: z.date(), isAvailable: z.boolean(), isBooked: z.boolean(), isBlocked: z.boolean(), blockReason: z.string().optional(), appointmentId: AppointmentIdSchema.optional() }); export const AppointmentSlotListResponseSchema = z.object({ slots: z.array(AppointmentSlotResponseSchema), pagination: z.object({ page: z.number().positive(), limit: z.number().positive(), total: z.number().nonnegative(), totalPages: z.number().nonnegative() }) }); export const AppointmentDetailResponseSchema = z.object({ appointment: AppointmentResponseSchema, patient: z.object({ id: z.string().uuid(), firstName: z.string(), lastName: z.string(), dateOfBirth: z.date(), phoneNumber: z.string().optional(), email: z.string().email().optional() }), provider: z.object({ id: z.string().uuid(), firstName: z.string(), lastName: z.string(), specialty: z.string().optional(), phoneNumber: z.string().optional(), email: z.string().email().optional() }), reminders: z.array(z.object({ type: z.enum(['email', 'sms', 'phone', 'push']), scheduledAt: z.date(), sentAt: z.date().optional(), status: z.enum(['scheduled', 'sent', 'failed', 'cancelled']) })).optional(), attachments: z.array(z.object({ id: z.string().uuid(), name: z.string(), type: z.enum(['document', 'image', 'video', 'audio']), url: z.string().url(), size: z.number().positive(), uploadedAt: z.date() })).optional(), tags: z.array(z.string()).optional() }); // ============================================================================ // FILTER/SEARCH SCHEMAS // ============================================================================ export const AppointmentSearchFiltersSchema = z.object({ query: z.string().optional(), patientId: z.string().uuid().optional(), providerId: z.string().uuid().optional(), type: AppointmentTypeSchema.optional(), status: AppointmentStatusSchema.optional(), startDate: z.date().optional(), endDate: z.date().optional(), hasDiagnosis: z.boolean().optional(), hasPrescriptions: z.boolean().optional(), hasProcedures: z.boolean().optional(), hasLabOrders: z.boolean().optional(), hasImagingOrders: z.boolean().optional(), hasReferrals: z.boolean().optional(), hasFollowUp: z.boolean().optional(), isCancelled: z.boolean().optional(), tags: z.array(z.string()).optional(), createdAfter: z.date().optional(), createdBefore: z.date().optional() }); export const AppointmentSlotSearchFiltersSchema = z.object({ providerId: z.string().uuid(), startDate: z.date(), endDate: z.date(), type: AppointmentTypeSchema.optional(), duration: z.number().positive().optional(), // in minutes isAvailable: z.boolean().optional() }); export const AppointmentSortOptionsSchema = z.enum([ 'startTime', 'endTime', 'createdAt', 'updatedAt', 'status', 'type', 'patientName', 'providerName' ]); // ============================================================================ // VALIDATION UTILITIES // ============================================================================ export const AppointmentValidationUtils = { isValidTimeSlot: (startTime, endTime) => { return startTime < endTime; }, calculateDuration: (startTime, endTime) => { const diffMs = endTime.getTime() - startTime.getTime(); const diffMinutes = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMinutes / 60); const diffDays = Math.floor(diffHours / 24); return { minutes: diffMinutes % 60, hours: diffHours % 24, days: diffDays }; }, isBusinessHours: (date) => { const hour = date.getHours(); const day = date.getDay(); // Monday to Friday, 8 AM to 6 PM return day >= 1 && day <= 5 && hour >= 8 && hour < 18; }, isAvailableTimeSlot: (startTime, endTime, existingAppointments) => { for (const appointment of existingAppointments) { if ((startTime >= appointment.startTime && startTime < appointment.endTime) || (endTime > appointment.startTime && endTime <= appointment.endTime) || (startTime <= appointment.startTime && endTime >= appointment.endTime)) { return false; } } return true; }, validateAppointmentData: (data) => { const errors = []; if (data.startTime && data.endTime && data.startTime >= data.endTime) { errors.push('Start time must be before end time'); } if (data.startTime && data.startTime < new Date()) { errors.push('Start time cannot be in the past'); } if (data.recurringPattern) { if (data.recurringPattern.frequency === 'weekly' && (!data.recurringPattern.daysOfWeek || data.recurringPattern.daysOfWeek.length === 0)) { errors.push('Weekly recurring appointments must specify days of week'); } if (data.recurringPattern.frequency === 'monthly' && !data.recurringPattern.dayOfMonth) { errors.push('Monthly recurring appointments must specify day of month'); } if (data.recurringPattern.frequency === 'yearly' && !data.recurringPattern.monthOfYear) { errors.push('Yearly recurring appointments must specify month of year'); } } if (data.reminders) { data.reminders.forEach((reminder, index) => { if (reminder.minutesBefore <= 0) { errors.push(`Reminder ${index + 1}: minutes before must be positive`); } }); } return { isValid: errors.length === 0, errors }; } }; // ============================================================================ // SCHEMA EXPORTS // ============================================================================ export const AppointmentSchemas = { // Base schemas AppointmentId: AppointmentIdSchema, TimeSlot: TimeSlotSchema, Duration: DurationSchema, // Enum schemas AppointmentStatus: AppointmentStatusSchema, AppointmentType: AppointmentTypeSchema, // Value object schemas AppointmentIdValueObject: AppointmentIdValueObjectSchema, TimeSlotValueObject: TimeSlotValueObjectSchema, DurationValueObject: DurationValueObjectSchema, // Entity schemas Appointment: AppointmentSchema, AppointmentSlot: AppointmentSlotSchema, // Request schemas CreateAppointmentRequest: CreateAppointmentRequestSchema, UpdateAppointmentRequest: UpdateAppointmentRequestSchema, CancelAppointmentRequest: CancelAppointmentRequestSchema, RescheduleAppointmentRequest: RescheduleAppointmentRequestSchema, AddDiagnosisRequest: AddDiagnosisRequestSchema, AddPrescriptionRequest: AddPrescriptionRequestSchema, AddProcedureRequest: AddProcedureRequestSchema, AddLabOrderRequest: AddLabOrderRequestSchema, AddImagingOrderRequest: AddImagingOrderRequestSchema, AddReferralRequest: AddReferralRequestSchema, ScheduleFollowUpRequest: ScheduleFollowUpRequestSchema, // Response schemas AppointmentResponse: AppointmentResponseSchema, AppointmentListResponse: AppointmentListResponseSchema, AppointmentSlotResponse: AppointmentSlotResponseSchema, AppointmentSlotListResponse: AppointmentSlotListResponseSchema, AppointmentDetailResponse: AppointmentDetailResponseSchema, // Filter schemas AppointmentSearchFilters: AppointmentSearchFiltersSchema, AppointmentSlotSearchFilters: AppointmentSlotSearchFiltersSchema, AppointmentSortOptions: AppointmentSortOptionsSchema, // Utilities ValidationUtils: AppointmentValidationUtils }; //# sourceMappingURL=appointment-schemas.js.map