fix financial and certificates

This commit is contained in:
root
2026-06-05 01:51:08 -04:00
parent d28d11e2e5
commit ad968eaff7
94 changed files with 9654 additions and 214 deletions
@@ -0,0 +1,99 @@
<?php
namespace App\Services\Grading\Validation;
use Illuminate\Support\Facades\DB;
class GradebookFinalizationValidator
{
/**
* Strong-mode validation only. Legacy mode deliberately remains displayable/lockable
* so historical records are not reinterpreted by the new policy.
*/
public function validateClassSectionBeforeLock(int $classSectionId, string $semester, string $schoolYear): array
{
$errors = [];
foreach ($this->scoreTables() as $table => $meta) {
if (!DB::getSchemaBuilder()->hasTable($table)) {
continue;
}
$query = DB::table($table)
->where('class_section_id', $classSectionId)
->where('semester', $semester)
->where('school_year', $schoolYear);
if (DB::getSchemaBuilder()->hasColumn($table, 'status')) {
$pending = (clone $query)->where(function ($q) {
$q->whereNull('status')->orWhere('status', 'pending');
})->get();
foreach ($pending as $row) {
$errors[] = $this->error($table, $meta['category'], $row, 'pending_score', 'Score must be marked scored, missing, excused, or not_assigned before strong-mode lock.');
}
}
if (DB::getSchemaBuilder()->hasColumn($table, 'max_points')) {
$invalidMax = (clone $query)->where(function ($q) {
$q->whereNull('max_points')->orWhere('max_points', '<=', 0);
})->get();
foreach ($invalidMax as $row) {
$errors[] = $this->error($table, $meta['category'], $row, 'invalid_max_points', 'Max points must be greater than 0.');
}
$invalidScores = (clone $query)
->whereNotNull('score')
->where(function ($q) {
$q->where('score', '<', 0)->orWhereRaw('score > max_points');
})
->get();
foreach ($invalidScores as $row) {
$errors[] = $this->error($table, $meta['category'], $row, 'score_out_of_range', 'Score must be between 0 and max_points.');
}
} else {
$invalidScores = (clone $query)
->whereNotNull('score')
->where(function ($q) {
$q->where('score', '<', 0)->orWhere('score', '>', 100);
})
->get();
foreach ($invalidScores as $row) {
$errors[] = $this->error($table, $meta['category'], $row, 'score_out_of_range', 'Score must be between 0 and 100.');
}
}
}
return [
'valid' => empty($errors),
'errors' => $errors,
];
}
private function scoreTables(): array
{
return [
'homework' => ['category' => 'homework'],
'quiz' => ['category' => 'quiz'],
'project' => ['category' => 'project'],
'participation' => ['category' => 'participation'],
'midterm_exam' => ['category' => 'midterm_exam'],
'final_exam' => ['category' => 'final_exam'],
];
}
private function error(string $table, string $category, object $row, string $code, string $message): array
{
return [
'table' => $table,
'category' => $category,
'student_id' => isset($row->student_id) ? (int) $row->student_id : null,
'item_index' => $row->homework_index ?? $row->quiz_index ?? $row->project_index ?? null,
'code' => $code,
'message' => $message,
];
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Services\Grading\Validation;
use InvalidArgumentException;
class ScoreValueValidator
{
public const DEFAULT_MAX_POINTS = 100.0;
public function normalizeNullable(mixed $value, float|int|null $maxPoints = self::DEFAULT_MAX_POINTS, string $label = 'Score'): ?float
{
if ($value === null || (is_string($value) && trim($value) === '')) {
return null;
}
$max = $this->normalizeMaxPoints($maxPoints, $label);
if (!is_numeric($value)) {
throw new InvalidArgumentException("{$label} must be numeric.");
}
$score = (float) $value;
if ($score < 0 || $score > $max) {
throw new InvalidArgumentException("{$label} must be between 0 and {$max}.");
}
return $score;
}
public function normalizeMaxPoints(float|int|null $maxPoints = self::DEFAULT_MAX_POINTS, string $label = 'Score'): float
{
$max = $maxPoints === null ? self::DEFAULT_MAX_POINTS : (float) $maxPoints;
if ($max <= 0) {
throw new InvalidArgumentException("{$label} max points must be greater than 0.");
}
return $max;
}
public function inferStatus(?float $score, bool $missingAllowed = false): string
{
if ($score !== null) {
return 'scored';
}
return $missingAllowed ? 'excused' : 'pending';
}
}