Skip to main content

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

  1. Module Structure
  2. Appointment Endpoints
  3. Core Features
  4. Appointment Lifecycle
  5. Slot Management
  6. Payment Integration
  7. Notification System
  8. Business Rules
  9. Background Processing
  10. 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

EndpointMethodDescriptionRolesAuth Type
/appointment/createPOSTCreate new appointmentTHERAPIST, ADMINJWT
/appointment/start-paymentPOSTInitiate payment processPARENTJWT
/appointment/cancelPOSTCancel appointmentPARENT, THERAPIST, ADMINJWT
/appointment/approvePOSTApprove/reject appointmentTHERAPIST, ADMINJWT
/appointment/rebookPOSTRebook cancelled appointmentTHERAPIST, ADMINJWT
/appointment/reschedulePOSTReschedule confirmed appointmentTHERAPIST, ADMINJWT
/appointment/confirm-paymentPOSTConfirm payment completionPARENTJWT
/appointment/auto-rejectPOSTAuto-reject pending appointmentsNoneNONE
/appointment/complete-statusPOSTMark appointment as completedNoneNONE
/appointment/defrozen-slotsPOSTRelease frozen time slotsNoneNONE
/appointment/add-note-webhookPOSTHandle session notes updatesNoneNONE
/appointment/upcoming-notificationPOSTSend upcoming appointment alertsNoneNONE

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 payment
  • CONFIRMED: Payment completed, awaiting therapist approval
  • APPROVED: Therapist approved, appointment confirmed (Deprecated)
  • REJECTED: Therapist rejected, full refund initiated (Deprecated)
  • Failed: Payment attempt failed (Deprecated)
  • STARTED: Appointment session has begun
  • COMPLETED: Session finished successfully
  • CANCELLED: Cancelled by parent/therapist with partial refund
  • EXPIRED: Payment link expired

Payment Statuses:

  • PENDING: Payment link created, awaiting completion
  • SUCCESSFUL: Payment captured successfully
  • FAILED: Payment attempt failed
  • EXPIRED: 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:

  1. Payment Link Creation: Generate Razorpay payment link with 24-hour or appointment-time expiry
  2. Payment Initiation: Parent starts payment process
  3. Payment Confirmation: Verify payment signature and update status
  4. Slot Confirmation: Mark time slots as booked
  5. 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;
// 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.