Commit f1091d30 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix missing attendance records: generate on session creation

SessionCreated event had no listener to generate attendance records for
existing enrollments. Added GenerateSessionAttendance listener, fixed
rescheduleSession to dispatch SessionCreated, added attendance generation
on payment-activated enrollments, and added daily backfill command as safety net.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 96cb843e
<?php
namespace App\Console\Commands;
use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\Training\Models\Enrollment;
use App\Domain\Training\Models\TrainingSession;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class BackfillAttendanceRecords extends Command
{
protected $signature = 'attendance:backfill {--group= : Specific group ID to backfill}';
protected $description = 'Generate missing attendance records for all active enrollments and scheduled sessions';
public function handle(): int
{
$groupFilter = $this->option('group');
$query = TrainingSession::query()
->whereIn('status', ['scheduled', 'in_progress']);
if ($groupFilter) {
$query->where('training_group_id', $groupFilter);
}
$sessions = $query->get();
$inserted = 0;
$subjectType = \App\Domain\Participant\Models\Participant::class;
foreach ($sessions as $session) {
$enrollments = Enrollment::where('training_group_id', $session->training_group_id)
->where('status', 'active')
->where('enrollment_date', '<=', $session->session_date)
->whereHas('participant', function ($q) {
$q->whereNotIn('status', ['frozen', 'suspended']);
})
->get();
foreach ($enrollments as $enrollment) {
$exists = AttendanceRecord::where('training_session_id', $session->id)
->where('subject_type', $subjectType)
->where('subject_id', $enrollment->participant_id)
->exists();
if (!$exists) {
AttendanceRecord::create([
'academy_id' => $session->academy_id,
'training_session_id' => $session->id,
'subject_type' => $subjectType,
'subject_id' => $enrollment->participant_id,
'status' => 'expected',
'is_auto_generated' => true,
]);
$inserted++;
}
}
}
if ($inserted > 0) {
$this->info("Backfilled {$inserted} missing attendance records.");
} else {
$this->info("No missing attendance records found.");
}
return self::SUCCESS;
}
}
...@@ -19,12 +19,16 @@ public function handle(InvoicePaid $event): void ...@@ -19,12 +19,16 @@ public function handle(InvoicePaid $event): void
->where('status', 'pending') ->where('status', 'pending')
->get(); ->get();
$attendanceService = app(\App\Domain\Attendance\Services\AttendanceGenerationService::class);
foreach ($pendingEnrollments as $enrollment) { foreach ($pendingEnrollments as $enrollment) {
$enrollment->update([ $enrollment->update([
'status' => 'active', 'status' => 'active',
'payment_status' => 'paid', 'payment_status' => 'paid',
'activated_at' => now(), 'activated_at' => now(),
]); ]);
$attendanceService->generateForEnrollment($enrollment);
} }
// Mark payment_status as paid for active enrollments linked to this invoice // Mark payment_status as paid for active enrollments linked to this invoice
......
<?php
namespace App\Domain\Training\Listeners;
use App\Domain\Attendance\Services\AttendanceGenerationService;
use App\Domain\Training\Events\SessionCreated;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class GenerateSessionAttendance implements ShouldQueue
{
public function __construct(
private AttendanceGenerationService $attendanceService,
) {}
public function handle(SessionCreated $event): void
{
try {
$this->attendanceService->generateForSession($event->session);
} catch (\Throwable $e) {
Log::error('GenerateSessionAttendance failed: ' . $e->getMessage(), [
'session_id' => $event->session->id,
'group_id' => $event->session->training_group_id,
]);
}
}
public function failed(SessionCreated $event, \Throwable $exception): void
{
Log::critical('GenerateSessionAttendance PERMANENTLY FAILED', [
'session_id' => $event->session->id,
'error' => $exception->getMessage(),
]);
}
}
...@@ -168,6 +168,8 @@ public function rescheduleSession(TrainingSession $session, string $newDate, str ...@@ -168,6 +168,8 @@ public function rescheduleSession(TrainingSession $session, string $newDate, str
'cancelled_by' => $actor->id, 'cancelled_by' => $actor->id,
]); ]);
SessionCreated::dispatch($newSession);
return $newSession; return $newSession;
}); });
} }
......
...@@ -66,6 +66,7 @@ class EventServiceProvider extends ServiceProvider ...@@ -66,6 +66,7 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Training\Listeners\HandleEnrollmentTransfer::class, \App\Domain\Training\Listeners\HandleEnrollmentTransfer::class,
], ],
\App\Domain\Training\Events\SessionCreated::class => [ \App\Domain\Training\Events\SessionCreated::class => [
\App\Domain\Training\Listeners\GenerateSessionAttendance::class,
\App\Domain\Training\Listeners\CreateAutoReservation::class, \App\Domain\Training\Listeners\CreateAutoReservation::class,
], ],
\App\Domain\Training\Events\SessionCancelled::class => [ \App\Domain\Training\Events\SessionCancelled::class => [
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
Schedule::command('attendance:send-alerts')->dailyAt('20:00'); Schedule::command('attendance:send-alerts')->dailyAt('20:00');
Schedule::command('invoices:mark-overdue')->dailyAt('01:00'); Schedule::command('invoices:mark-overdue')->dailyAt('01:00');
Schedule::command('sessions:generate-upcoming')->dailyAt('02:00'); Schedule::command('sessions:generate-upcoming')->dailyAt('02:00');
Schedule::command('attendance:backfill')->dailyAt('02:30');
Schedule::command('audit:cleanup --days=365')->weekly(); Schedule::command('audit:cleanup --days=365')->weekly();
Schedule::command('documents:expire')->dailyAt('06:00'); Schedule::command('documents:expire')->dailyAt('06:00');
Schedule::command('summary:daily')->dailyAt('07:00'); Schedule::command('summary:daily')->dailyAt('07:00');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment