Commit b2e50198 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Never auto-remove participants: only admin action can change status

Business rule: non-payment or absence = debt, not removal. Participants
stay enrolled and appear in attendance regardless of payment/attendance.

- Remove frozen/suspended filter from attendance generation
- DeactivateExpiredEnrollments now only logs (no status change)
- EnforceAttendanceThresholds only warns (no auto-suspend)
- SuspendOnThreshold listener disabled (log only)
- BackfillAttendanceRecords no longer skips frozen/suspended
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent f1091d30
...@@ -33,9 +33,6 @@ public function handle(): int ...@@ -33,9 +33,6 @@ public function handle(): int
$enrollments = Enrollment::where('training_group_id', $session->training_group_id) $enrollments = Enrollment::where('training_group_id', $session->training_group_id)
->where('status', 'active') ->where('status', 'active')
->where('enrollment_date', '<=', $session->session_date) ->where('enrollment_date', '<=', $session->session_date)
->whereHas('participant', function ($q) {
$q->whereNotIn('status', ['frozen', 'suspended']);
})
->get(); ->get();
foreach ($enrollments as $enrollment) { foreach ($enrollments as $enrollment) {
......
...@@ -17,13 +17,19 @@ public function handle(): int ...@@ -17,13 +17,19 @@ public function handle(): int
->where('end_date', '<', now()->toDateString()) ->where('end_date', '<', now()->toDateString())
->get(); ->get();
$count = 0; if ($expired->isEmpty()) {
$this->info('No enrollments past their end date.');
return self::SUCCESS;
}
// Log only — never auto-remove participants. Only admin action can cancel/expire.
$this->info("Found {$expired->count()} enrollment(s) past end_date (no action taken — admin must handle manually):");
foreach ($expired as $enrollment) { foreach ($expired as $enrollment) {
$enrollment->update(['status' => 'expired']); $name = $enrollment->participant?->person?->name_ar ?? "#{$enrollment->participant_id}";
$count++; $group = $enrollment->group?->name_ar ?? "#{$enrollment->training_group_id}";
$this->line(" - {$name} in {$group} (end_date: {$enrollment->end_date->format('Y-m-d')})");
} }
$this->info("Deactivated {$count} expired enrollments.");
return self::SUCCESS; return self::SUCCESS;
} }
} }
...@@ -19,11 +19,8 @@ class EnforceAttendanceThresholds extends Command ...@@ -19,11 +19,8 @@ class EnforceAttendanceThresholds extends Command
public function handle(SettingsService $settings): int public function handle(SettingsService $settings): int
{ {
if (!(bool) $settings->get('auto_suspend_on_threshold', false)) { // Never auto-suspend — participants stay enrolled regardless.
$this->info('Auto-suspend is disabled (auto_suspend_on_threshold = false). Skipping.'); // Only report for admin awareness.
return self::SUCCESS;
}
$this->checkConsecutiveAbsences(); $this->checkConsecutiveAbsences();
$this->checkAttendancePercentage(); $this->checkAttendancePercentage();
...@@ -53,16 +50,11 @@ private function checkConsecutiveAbsences(): void ...@@ -53,16 +50,11 @@ private function checkConsecutiveAbsences(): void
); );
if ($allAbsent) { if ($allAbsent) {
$participant->update([ Log::warning("Participant #{$participant->id} has {$maxAbsences} consecutive absences — flagged for admin review");
'status' => 'suspended', $this->warn(__('تنبيه غياب: المشترك #:id (:name) — :count غيابات متتالية', [
'status_reason' => __('تعليق تلقائي: :count غيابات متتالية', ['count' => $maxAbsences]),
'status_changed_at' => now(),
]);
Log::warning("Participant #{$participant->id} auto-suspended: {$maxAbsences} consecutive absences");
$this->warn(__('تم تعليق المشترك #:id (:name)', [
'id' => $participant->id, 'id' => $participant->id,
'name' => $participant->person?->name_ar ?? '-', 'name' => $participant->person?->name_ar ?? '-',
'count' => $maxAbsences,
])); ]));
} }
} }
......
...@@ -17,35 +17,12 @@ public function __construct( ...@@ -17,35 +17,12 @@ public function __construct(
public function handle(AttendanceThresholdBreached $event): void public function handle(AttendanceThresholdBreached $event): void
{ {
try { // Never auto-suspend — participants stay enrolled regardless of attendance.
if (!(bool) $this->settings->get('auto_suspend_on_threshold', false)) { // Only admin can change participant status manually.
return; Log::info('Attendance threshold breached (no auto-action taken)', [
} 'participant_id' => $event->participant->id,
'rate' => $event->rate ?? null,
$participant = $event->participant; ]);
if ($participant->status === 'suspended' || $participant->status === 'blacklisted') {
return;
}
$statusValue = $participant->status instanceof \BackedEnum
? $participant->status->value
: (string) $participant->status;
if (!in_array($statusValue, ['active'])) {
return;
}
$this->participantService->changeStatus(
$participant,
'suspended',
"تجاوز حد الغياب المسموح - نسبة الحضور: {$event->rate}%",
);
} catch (\Throwable $e) {
Log::error('SuspendOnThreshold failed: ' . $e->getMessage(), [
'participant_id' => $event->participant->id,
]);
}
} }
public function failed(AttendanceThresholdBreached $event, \Throwable $exception): void public function failed(AttendanceThresholdBreached $event, \Throwable $exception): void
......
...@@ -26,8 +26,7 @@ public function generateForSession(TrainingSession $session): int ...@@ -26,8 +26,7 @@ public function generateForSession(TrainingSession $session): int
foreach ($enrollments as $enrollment) { foreach ($enrollments as $enrollment) {
$participant = $enrollment->participant; $participant = $enrollment->participant;
// Skip frozen participants if (!$participant) {
if (!$participant || in_array($participant->status->value ?? $participant->status, ['frozen', 'suspended'])) {
continue; continue;
} }
......
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