Appointment Module
The Appointment module is the core scheduling system for the Comdeall platform, managing the complete lifecycle of therapy sessions between children, parents, and therapists. It handles appointment creation, slot validation, payment processing, scheduling conflicts, cancellations, approvals, and automated notifications throughout the entire appointment journey.
Table of Contents
- Module Structure
- Appointment Endpoints
- Core Features
- Appointment Lifecycle
- Slot Management
- Payment Integration
- Notification System
- Business Rules
- Background Processing
- Error Handling
Module Structure
@Module({
imports: [forwardRef(() => PaymentModule), forwardRef(() => BackgroundModule), forwardRef(() => DBModule)],
controllers: [AppointmentsController],
providers: [AppointmentsService, JwtService],
exports: [AppointmentsService]
})
export class AppointmentsModule {}
Core Components:
- AppointmentsController: REST API endpoints for appointment operations
- AppointmentsService: Business logic for scheduling, payments, and validation
- AppointmentsDBService: Database operations coordination
- Payment Integration: Razorpay payment processing and refunds
- Background Jobs: Scheduled notifications and automated tasks
Appointment Endpoints
| Endpoint | Method | Description | Roles | Auth Type |
|---|---|---|---|---|
/appointment/create | POST | Create new appointment | THERAPIST, ADMIN | JWT |
/appointment/start-payment | POST | Initiate payment process | PARENT | JWT |
/appointment/cancel | POST | Cancel appointment | PARENT, THERAPIST, ADMIN | JWT |
/appointment/approve | POST | Approve/reject appointment | THERAPIST, ADMIN | JWT |
/appointment/rebook | POST | Rebook cancelled appointment | THERAPIST, ADMIN | JWT |
/appointment/reschedule | POST | Reschedule confirmed appointment | THERAPIST, ADMIN | JWT |
/appointment/confirm-payment | POST | Confirm payment completion | PARENT | JWT |
/appointment/auto-reject | POST | Auto-reject pending appointments | None | NONE |
/appointment/complete-status | POST | Mark appointment as completed | None | NONE |
/appointment/defrozen-slots | POST | Release frozen time slots | None | NONE |
/appointment/add-note-webhook | POST | Handle session notes updates | None | NONE |
/appointment/upcoming-notification | POST | Send upcoming appointment alerts | None | NONE |
Core Features
Appointment Types
export enum AppointmentType {
ONE_ON_ONE = 'ONE_ON_ONE',
TEAM_MEETING = 'TEAM_MEETING',
}
- One-on-One: Individual therapy sessions between child and single therapist
- Team Meeting: Multi-therapist collaborative sessions for complex cases
Appointment Statuses
Lifecycle Statuses:
SCHEDULED: Initial status after creation, pending paymentCONFIRMED: Payment completed, awaiting therapist approvalAPPROVED: Therapist approved, appointment confirmed (Deprecated)REJECTED: Therapist rejected, full refund initiated (Deprecated)Failed: Payment attempt failed (Deprecated)STARTED: Appointment session has begunCOMPLETED: Session finished successfullyCANCELLED: Cancelled by parent/therapist with partial refundEXPIRED: Payment link expired
Payment Statuses:
PENDING: Payment link created, awaiting completionSUCCESSFUL: Payment captured successfullyFAILED: Payment attempt failedEXPIRED: Payment link expired
Multi-Therapist Support
The system supports multiple therapists per appointment:
// Therapist assignment and validation
if (appointment_data.other_therapists.length === 0) {
throw new BadRequestException('At least one therapist is required');
}
// Validate all assigned therapists are active and approved
for (const therapist of therapists) {
if (therapist.is_deleted ||
therapist.approval_status !== 'APPROVED' ||
!therapist.users.is_onboarded) {
throw new BadRequestException(`Therapist ${therapist.users.name} is not active`);
}
}
Appointment Lifecycle
1. Creation & Validation
async createAppointment(appointment_data: CreateAppointmentDto, user_type: UserRole, therapist_id?: string) {
// 1. Validate meet link format
if (!isValidMeetLink(appointment_data.meet_link)) {
throw new BadRequestException('Invalid meet link format');
}
// 2. Validate therapist assignments and availability
// 3. Check continuous slot availability for all therapists
// 4. Validate no scheduling conflicts exist
// 5. Calculate fees and charges
// 6. Create payment link with Razorpay
// 7. Save appointment with SCHEDULED status
// 8. Queue payment expiry job
}
Key Validations:
- Slot Duration: Minimum 15 minutes per slot
- Therapist Status: Must be approved and onboarded
- Continuous Availability: All therapists must be available for entire duration
- Conflict Detection: No overlapping appointments
- Assessment Linking: Optional assessment must belong to same child
2. Payment Processing
Payment Flow:
- Payment Link Creation: Generate Razorpay payment link with 24-hour or appointment-time expiry
- Payment Initiation: Parent starts payment process
- Payment Confirmation: Verify payment signature and update status
- Slot Confirmation: Mark time slots as booked
- Notification Queue: Schedule appointment and reminder notifications
// Payment confirmation with retry logic
let attempt = 0;
while (attempt < this.retryCount) {
const payment_details = await this.paymentService.getPaymentDetails(payment_data.provider_id);
if (payment_response.status === 'captured') break;
attempt++;
await sleep(Math.pow(2, attempt) * 1000); // Exponential backoff
}
3. Therapist Approval (Deprecated)
async appointmentApproval(approval_data: ApprovalDataDto, therapist_id: string) {
// Validation checks
if (appointment.approval_status === 'APPROVED') {
throw new BadRequestException('Appointment already approved');
}
if (!approval_data.is_approved) {
// Rejection: Process full refund
const refund_response = await this.paymentService.initiateRefund({
amount: amount * 100,
payment_id: payment.provider_id,
speed: RefundSpeed.NORMAL
});
// Deduct penalty from therapist wallet (25% of consultation fee)
if (isPubSub) {
const penalty = consultation_fee * 0.25;
await this.deductFromTherapistWallet(therapist_id, penalty);
}
}
}
Approval Business Rules:
- 24-Hour Auto-Reject: Appointments auto-rejected if not approved within 24 hours
- Rejection Penalties: Therapists lose 25% of consultation fee for rejections
- Full Refunds: Parents receive complete refund for rejected appointments
4. Cancellation Logic
async cancelAppointment(appointment_data: CancelAppointmentDto) {
// Time-based cancellation rules
if (new Date(new Date().getTime() + this.cancellationBlockWindowMinutes * 60 * 1000) > appointmentDate) {
throw new BadRequestException('Cannot cancel appointment within 24 hours');
}
// Refund calculations based on cancellation initiator
if (appointment_data.userType === UserRole.PARENT) {
refund_amt = amount - cancellation_charge; // Parent pays ₹250 cancellation fee
therapist_wallet_credit = consultation_fee; // Therapist loses earning
} else if (appointment_data.userType === UserRole.THERAPIST) {
refund_amt = amount; // Full refund to parent
therapist_wallet_debit = consultation_fee + cancellation_charge; // Therapist penalty
}
}
Cancellation Rules:
- 24-Hour Window: Cannot cancel within 24 hours of appointment
- Cancellation Fees: ₹250 charge for parent-initiated cancellations
- Therapist Penalties: Therapists pay consultation fee + ₹250 for cancellations
- Wallet Adjustments: Automatic wallet credit/debit based on cancellation type
- Meeting Type Restriction: Only one-on-one meetings are eligible for cancellation, team meetings cannot be cancelled
Slot Management
Continuous Slot Validation
private validateContinuousSlotsPerTherapist(availableSlots, startTime, endTime, therapistMap) {
for (const therapistId of therapistIds) {
const slots = availableSlots[therapistId] || [];
const sortedSlots = slots.sort((a, b) => a.StartTime.getTime() - b.StartTime.getTime());
let currentCoverage = startTime.getTime();
for (const slot of sortedSlots) {
const slotStart = Math.max(slot.StartTime.getTime(), startTime.getTime());
const slotEnd = Math.min(slot.EndTime.getTime(), endTime.getTime());
// Check for gaps in coverage
if (slotStart > currentCoverage) {
throw new BadRequestException(`${therapistName} unavailable from ${gapStart} to ${gapEnd}`);
}
currentCoverage = Math.max(currentCoverage, slotEnd);
}
}
}
Conflict Detection
private async validateSlotOverlap(startTime: Date, endTime: Date, therapistMap) {
const conflictingSlots = await this.appointmentsDBService.getConflictingSlots(
therapistIds, startTime, endTime
);
for (const conflict of conflictingSlots) {
const therapistName = therapistMap.get(conflict.therapist_id).users.name;
throw new BadRequestException(`${therapistName} has conflicting appointment`);
}
}
Reschedule Validation
async rescheduleAppointment(rescheduleDto: RescheduleDto) {
// Business rule validations
if (appointment.reschedule_count === 3) {
throw new BadRequestException('Already rescheduled 3 times - maximum limit reached');
}
if (new Date().getTime() + (24 * 60 * 60 * 1000) > appointmentStart) {
throw new BadRequestException('Cannot reschedule within 24 hours of appointment');
}
// Duration must match original appointment
if (existingDuration !== newSlotsDuration) {
throw new BadRequestException('New slot duration must match original appointment');
}
}
Payment Integration
Fee Calculation System
// Dynamic fee calculation with multiple charge types
let total_consultation_fee = 0;
for (const therapist of therapists) {
const feePerHour = Number(therapist.consultation_fees ?? 0);
const proratedFee = roundToTwoDecimals((feePerHour * totalDurationMins) / 60);
total_consultation_fee += proratedFee;
}
const service_fee = total_consultation_fee * (platform_fee_percent / 100);
const taxes = total_consultation_fee * (gst_percent / 100);
// Process additional charges (ADD/SUBTRACT operations)
other_charges.forEach((charge) => {
if (charge.operation === 'ADD') {
additional_charges += charge.value;
} else {
additional_charges -= charge.value;
}
});
const final_amount = total_consultation_fee + service_fee + taxes + additional_charges;
Payment Link Generation
// Razorpay payment link with smart expiry
const expire_by = Math.min(
Math.floor(addMinutes(new Date(), 24 * 60).getTime() / 1000), // 24 hours
Math.floor(appointmentStartTime.getTime() / 1000) // Or appointment time
);
const link_data = {
amount: final_amount * 100, // Razorpay expects amount in paise
description: `Appointment of ${child_name} with therapist${therapists.length > 1 ? 's' : ''}`,
expire_by,
customer: {
name: parent_name,
email: parent_email,
contact: local_phone_number
}
};
Refund Processing
// Intelligent refund calculation
const refund_data = {
amount: refund_amount * 100,
payment_id: original_payment.provider_id,
speed: RefundSpeed.NORMAL,
notes: {
organization: this.organization,
transaction_id: refund_transaction.id,
}
};
const refund_response = await this.paymentService.initiateRefund(refund_data);
Notification System
Automated Notification Triggers
// Welcome notification after successful payment
await this.backgroundServiceManager.addSendCustomNotificationJob(JobName.SEND_CUSTOM_NOTIFICATION, {
user_ids: therapist_ids,
subject: `New session booked: ${child_name} on ${date} at ${time}`,
description: 'New Session Booking Notification'
});
// Reminder notifications (1 hour before appointment)
for (const slot of appointment_slots) {
const reminder_delay = slot.start_time.getTime() - new Date().getTime() - (60 * 60 * 1000);
await this.backgroundServiceManager.addSendCustomNotificationJob(
JobName.SEND_CUSTOM_NOTIFICATION,
{
user_ids: [parent_id],
subject: 'Session Reminder',
description: `Your session with ${therapist_names} starts at ${time}`
},
`appointment-remainder:${appointment_id}:${slot.start_time.toISOString()}`,
reminder_delay
);
}
Admin Notifications
// Notify administrators of new bookings
const admin_notifications: AdminNotificationInput[] = admin_ids.map(admin => ({
user_id: admin.id,
subject: 'New Appointment Created',
body: `New appointment booked for ${child_name} with ${therapist_names.join(', ')}`,
is_read: false
}));
await this.appointmentsDBService.insertAdminNotification(admin_notifications);
Business Rules
Time-Based Restrictions
// Configuration-driven time windows
private readonly rescheduleBlockWindowMinutes: number; // 24 hours default
private readonly cancellationBlockWindowMinutes: number; // 24 hours default
Key Time Rules:
- 24-Hour Cancellation Window: No cancellations within 24 hours
- 24-Hour Reschedule Window: No rescheduling within 24 hours
- Auto-Reject Timer: Appointments auto-rejected after 24 hours without approval
- Payment Expiry: Payment links expire after 24 hours or at appointment time
Financial Rules
Cancellation Charges:
- Parent cancellation: ₹250 + loss of full payment
- Therapist cancellation: Full refund to parent + therapist penalty
Therapist Wallet System:
- Automatic credit for completed appointments
- Penalty deductions for cancellations and rejections
- Real-time balance updates
Appointment Limits
// Reschedule limitations
if (appointment.reschedule_count === 3) {
throw new BadRequestException('Maximum 3 reschedules allowed per appointment');
}
Background Processing
Scheduled Jobs
// Payment expiry job
const delay = payment_expiry.getTime() - new Date().getTime();
await this.backgroundServiceManager.addExpireAppointmentJob(
`appointment:${appointment_id}:expire`, delay
);
// Appointment lifecycle jobs
await this.backgroundServiceManager.addStartAppointmentJob(`appointment-start:${appointment_id}`, startDelay);
await this.backgroundServiceManager.addEndAppointmentJob(`appointment-end:${appointment_id}`, endDelay);
Cron Jobs
// Daily check for missing session notes
async checkMissingSessionNotesCron(): Promise<void> {
const pastAppointments = await this.appointmentsDBService.getPastCompletedAppointmentsWithoutNotes();
for (const appointment of pastAppointments) {
await this.sendMissingSessionNoteNotification(appointment);
}
}
// Auto-reject pending appointments after 24 hours
async autoReject(): Promise<IApiResponse> {
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const pendingAppointments = await this.appointmentsDBService.getRejectAppointments(twentyFourHoursAgo);
for (const appointment of pendingAppointments) {
await this.appointmentApproval(rejectData, appointment.therapist_id, UserRole.THERAPIST, true);
}
}
Error Handling
Validation Errors
// Comprehensive appointment validation
if (slotDuration < 15) {
throw new BadRequestException('Slot duration must be at least 15 minutes');
}
if (!isValidMeetLink(meet_link)) {
throw new BadRequestException('Invalid Google Meet link format');
}
if (appointment_type === AppointmentType.TEAM_MEETING && !other_therapists?.length) {
throw new BadRequestException('Team meetings require multiple therapists');
}
Payment Error Handling
// Razorpay payment verification with retry logic
if (payment_response.status === 'failed') {
// Clean up slots and notify failure
await this.appointmentsDBService.releaseSlots(appointment_id, therapist_ids, slot_times);
throw new BadRequestException('Payment failed - slots have been released');
}
// Signature verification for security
const verification_response = await this.paymentService.verifyPayment({
payment_id: payment_data.provider_id,
order_id: payment_data.razorpay_order_id,
signature: payment_data.payment_signature
});
if (!verification_response.data.data.signatureIsValid) {
throw new BadRequestException('Payment signature verification failed');
}
Graceful Degradation
// Webhook error handling with success responses
async addNoteWebhook(body: any): Promise<{ success: boolean }> {
try {
const appointment_details = await this.appointmentsDBService.getAppointmentDetails(appointment_id);
if (shouldSendNotification) {
await this.backgroundServiceManager.addSendCustomNotificationJob(/* notification data */);
}
return { success: true };
} catch (error) {
this.logger.error(`Webhook processing failed: ${error.message}`);
return { success: false }; // Graceful failure
}
}
Key Features Summary
✅ Multi-therapist scheduling with continuous slot validation
✅ Intelligent payment processing with Razorpay integration
✅ Time-based business rules for cancellations and rescheduling
✅ Automated notification system throughout appointment lifecycle
✅ Financial management with wallet system and penalty handling
✅ Conflict detection and availability validation
✅ Background job processing for scheduled tasks
✅ Comprehensive audit trail for all appointment actions
✅ Flexible fee calculation with multiple charge types
✅ Graceful error handling with detailed validation messages
The Appointment module serves as the central nervous system for therapy scheduling in the Comdeall platform, ensuring seamless coordination between all stakeholders while maintaining strict business rules and financial integrity.