event_title) ->appendProperty(TextProperty::create('METHOD', 'REQUEST')) ->event($this->generateEvent($interview)); return $calendar->get(); } /** * Build the Spatie Event component for the given interview. */ public function generateEvent(Interview $interview): Event { $startsAt = $interview->scheduled_at ? Carbon::parse($interview->scheduled_at)->toDateTime() : now()->toDateTime(); $endsAt = $interview->expires_at ? Carbon::parse($interview->expires_at)->toDateTime() : Carbon::parse($startsAt)->addHours(2)->toDateTime(); $organizerEmail = config('mail.from.address') ?: 'no-reply@singlelogin.com'; $organizerName = config('mail.from.name') ?: 'SingleLogin Assessment Portal'; $event = Event::create($interview->event_title) ->uniqueIdentifier('sls-interview-'.$interview->submission_unique_id) ->status(EventStatus::Confirmed) ->startsAt($startsAt) ->endsAt($endsAt) ->organizer($organizerEmail, $organizerName) ->appendProperty(TextProperty::create('SEQUENCE', '0')) ->description($this->generateDescription($interview)); // Candidate Attendee: RSVP / Response is required if (! empty($interview->candidate_email)) { $event->attendee( email: $interview->candidate_email, name: $interview->candidate_name, participationStatus: ParticipationStatus::NeedsAction, requiresResponse: true ); } // Assigned Panelists / Interviewers $assignedUsers = $interview->assignedUsers(); foreach ($assignedUsers as $interviewer) { if (! empty($interviewer->email)) { $event->attendee( email: $interviewer->email, name: $interviewer->name, participationStatus: ParticipationStatus::Accepted, requiresResponse: false ); } } // 15-minute advance alert notification (RFC 5545 VALARM) $reminderMessage = sprintf( 'Interview Reminder: %s (%s) with %s starts in 15 minutes.', $interview->job_title ?? 'Candidate Assessment', $interview->round ?? 'R1', $interview->candidate_name ?? 'Candidate' ); $event->alertMinutesBefore(15, $reminderMessage); // Note: No organizer is specified on the event as per requirement return $event; } /** * Generate structured event description with credentials, proctoring guidelines, and portal links. */ public function generateDescription(Interview $interview): string { $jobTitle = $interview->job_title ?: 'Assessment'; $round = $interview->round ?: 'R1'; $candidateName = $interview->candidate_name ?: 'Candidate'; $loginUrl = route('interview.candidate.login'); $assignedNames = $interview->assignedUsers()->pluck('name')->implode(', '); if (empty($assignedNames)) { $assignedNames = 'Hiring Panel'; } $lines = [ 'Candidate Assessment Invitation', '----------------------------------------', "Candidate: {$candidateName}", "Position: {$jobTitle}", "Assessment Round: {$round}", 'Coding Language: '.strtoupper($interview->language ?? 'Python'), 'Access Duration: '.($interview->valid_hours ?? 2).' Hour(s)', '', 'CANDIDATE ACCESS & CREDENTIALS', '----------------------------------------', "Portal URL: {$loginUrl}", "Identifier: {$interview->candidate_email}", "Temporary Password: {$interview->temp_password}", "Unique Session ID: {$interview->submission_unique_id}", '', 'PANELISTS / REVIEWERS', '----------------------------------------', "Assigned Interviewers: {$assignedNames}", ]; if (! empty($interview->description)) { $lines[] = ''; $lines[] = 'ADDITIONAL INSTRUCTIONS'; $lines[] = '----------------------------------------'; $lines[] = $interview->description; } $lines[] = ''; $lines[] = 'IMPORTANT NOTES'; $lines[] = '----------------------------------------'; $lines[] = '- Please join the assessment room 5 minutes prior to the scheduled start time.'; $lines[] = '- Ensure your web camera and microphone are connected for live proctoring.'; $lines[] = '- This session contains an automated 15-minute advance reminder alarm.'; return implode("\n", $lines); } /** * Format a safe filename for the .ics attachment. */ public function generateFilename(Interview $interview): string { $cleanTitle = preg_replace('/[^A-Za-z0-9_-]/', '_', $interview->event_title); return 'invitation_'.substr($cleanTitle, 0, 80).'.ics'; } }