fix unit tests as well as missing code
API CI/CD / Validate (composer + pint) (push) Successful in 2m7s
API CI/CD / Test (PHPUnit) (push) Failing after 2m23s
API CI/CD / Build frontend assets (push) Successful in 2m18s
API CI/CD / Security audit (push) Successful in 31s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped

This commit is contained in:
root
2026-06-25 14:26:32 -04:00
parent fdfcd1f0e2
commit 940afe9319
115 changed files with 4554 additions and 290 deletions
@@ -699,24 +699,33 @@ class CompetitionWinnersAdminService
);
$quizIndex = (int) ($row->next_index ?? 1);
$scoreExpression = 'ROUND(CASE
WHEN (cs.score * 100.0 / NULLIF(ccw.question_count, 0)) < 0 THEN 0
WHEN (cs.score * 100.0 / NULLIF(ccw.question_count, 0)) > 100 THEN 100
ELSE (cs.score * 100.0 / NULLIF(ccw.question_count, 0))
END, 2)';
$commentExpression = DB::connection()->getDriverName() === 'sqlite'
? "'Competition ' || cs.competition_id || ' normalized from ' || cs.score || '/' || ccw.question_count"
: "CONCAT('Competition ', cs.competition_id, ' normalized from ', cs.score, '/', ccw.question_count)";
$now = now()->toDateTimeString();
$sql = <<<'SQL'
INSERT INTO quiz
(student_id, school_id, class_section_id, updated_by, quiz_index, score, comment, semester, school_year, created_at, updated_at)
SELECT
cs.student_id,
?,
cs.class_section_id,
?,
?,
ROUND(LEAST(100, GREATEST(0, (cs.score / NULLIF(ccw.question_count, 0)) * 100)), 2),
CONCAT('Competition ', cs.competition_id, ' normalized from ', cs.score, '/', ccw.question_count),
?,
?,
NOW(), NOW()
FROM competition_scores cs
JOIN competition_class_winners ccw
ON ccw.competition_id = cs.competition_id
$sql = <<<SQL
INSERT INTO quiz
(student_id, school_id, class_section_id, updated_by, quiz_index, score, comment, semester, school_year, created_at, updated_at)
SELECT
cs.student_id,
?,
cs.class_section_id,
?,
?,
{$scoreExpression},
{$commentExpression},
?,
?,
?, ?
FROM competition_scores cs
JOIN competition_class_winners ccw
ON ccw.competition_id = cs.competition_id
AND ccw.class_section_id = cs.class_section_id
WHERE cs.competition_id = ?
AND cs.class_section_id = ?
@@ -728,6 +737,8 @@ SQL;
$quizIndex,
$semester,
$schoolYear,
$now,
$now,
$competitionId,
$cid,
]);
@@ -187,6 +187,9 @@ class AttendanceQueryService
implode(',', array_fill(0, count($sundayDates), '?')).')';
foreach ($students as $student) {
$student = is_array($student) ? $student : (array) $student;
unset($student['created_by'], $student['updated_by'], $student['deleted_at']);
$studentId = (int) $student['id'];
$rows = AttendanceData::query()
@@ -23,6 +23,9 @@ class BadgeStudentLookupService
public function fetchStudentList(?string $schoolYear, array $selectedStudentIds = []): array
{
$selectedStudentIds = $this->formatter->normalizeIds($selectedStudentIds);
$classSectionAggregate = DB::connection()->getDriverName() === 'sqlite'
? 'GROUP_CONCAT(DISTINCT cs.class_section_name) as class_section_name'
: "GROUP_CONCAT(DISTINCT cs.class_section_name ORDER BY cs.class_section_name SEPARATOR ', ') as class_section_name";
$query = DB::table('students')
->leftJoin('student_class as sc', function ($join) use ($schoolYear) {
@@ -40,7 +43,7 @@ class BadgeStudentLookupService
'students.registration_grade',
'students.school_id',
'students.school_year',
DB::raw("GROUP_CONCAT(DISTINCT cs.class_section_name ORDER BY cs.class_section_name SEPARATOR ', ') as class_section_name"),
DB::raw($classSectionAggregate),
])
->where('students.is_active', 1)
->groupBy(
@@ -13,6 +13,10 @@ class BadgeUserLookupService
public function fetchStaffList(?string $schoolYear, array $selectedUserIds = []): array
{
$rolesAggregate = DB::connection()->getDriverName() === 'sqlite'
? "GROUP_CONCAT(DISTINCT CASE WHEN LOWER(roles.name) != 'parent' THEN roles.name END) as roles"
: "GROUP_CONCAT(DISTINCT CASE WHEN LOWER(roles.name) != 'parent' THEN roles.name END ORDER BY roles.name SEPARATOR ', ') as roles";
$query = DB::table('users')
->leftJoin('user_roles', 'user_roles.user_id', '=', 'users.id')
->leftJoin('roles', 'roles.id', '=', 'user_roles.role_id')
@@ -20,7 +24,7 @@ class BadgeUserLookupService
'users.id',
'users.firstname',
'users.lastname',
DB::raw("GROUP_CONCAT(DISTINCT CASE WHEN LOWER(roles.name) != 'parent' THEN roles.name END ORDER BY roles.name SEPARATOR ', ') as roles"),
DB::raw($rolesAggregate),
])
->groupBy('users.id', 'users.firstname', 'users.lastname');
@@ -69,7 +73,7 @@ class BadgeUserLookupService
}
$row = $query
->orderByRaw("FIELD(tc.position, 'main', 'ta')")
->orderByRaw("CASE tc.position WHEN 'main' THEN 0 WHEN 'ta' THEN 1 ELSE 2 END")
->orderByRaw('IFNULL(tc.updated_at,"0000-00-00 00:00:00") DESC')
->orderByDesc('tc.id')
->first();
+11 -4
View File
@@ -190,7 +190,7 @@ class ChargeService
];
}
$charge = EventCharges::query()->create([
$data = [
'event_id' => $eventId > 0 ? $eventId : null,
'event_name' => $eventId > 0 ? null : $eventName,
'parent_id' => $parentId,
@@ -200,9 +200,16 @@ class ChargeService
'amount' => round($amount, 2),
'school_year' => $schoolYear,
'semester' => $semester,
'created_by' => $actorId ?: null,
'updated_by' => $actorId ?: null,
]);
];
if (Schema::hasColumn('event_charges', 'created_by')) {
$data['created_by'] = $actorId ?: null;
}
if (Schema::hasColumn('event_charges', 'updated_by')) {
$data['updated_by'] = $actorId ?: null;
}
$charge = EventCharges::query()->create($data);
$this->invoiceGeneration->generateInvoice($parentId, $schoolYear);
@@ -5,6 +5,7 @@ namespace App\Services\ClassProgress;
use App\Models\ClassProgressAttachment;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class ClassProgressAttachmentService
{
@@ -48,6 +49,11 @@ class ClassProgressAttachmentService
$candidates = [];
if (str_starts_with($path, 'storage/')) {
$relative = ltrim(substr($path, strlen('storage/')), '/');
$disk = (string) config('progress.attachments.disk', 'public');
if (Storage::disk($disk)->exists($relative)) {
return Storage::disk($disk)->path($relative);
}
$candidates[] = storage_path('app/public/'.$relative);
}
@@ -86,6 +86,7 @@ class ClassProgressMutationService
'unit_title' => $unitTitle,
'covered' => $covered,
'homework' => $homework !== '' ? $homework : null,
'next_week_plan' => trim((string) ($payload['next_week_plan'] ?? '')),
'status' => $this->rules->defaultStatus(),
'support_needed' => $payload['support_needed'] ?? null,
'flags_json' => $this->rules->normalizeFlags($payload['flags'] ?? null),
@@ -241,6 +242,7 @@ class ClassProgressMutationService
'unit_title' => $unitTitle,
'covered' => $covered,
'homework' => $homework !== '' ? $homework : null,
'next_week_plan' => trim((string) ($payload['next_week_plan'] ?? $existing?->next_week_plan ?? '')),
];
if ($flagsPresent) {
+28 -1
View File
@@ -24,7 +24,7 @@ class OpenApiRouteExporter
$this->mergeRoute($base['paths'], $route);
}
return $base;
return $this->redactSensitiveAuthTerms($base);
}
private function mergeRoute(array &$paths, Route $route): void
@@ -129,4 +129,31 @@ class OpenApiRouteExporter
return 'Endpoint';
}
private function redactSensitiveAuthTerms(mixed $value): mixed
{
if (is_array($value)) {
$redacted = [];
foreach ($value as $key => $child) {
$redacted[$this->redactSensitiveAuthString((string) $key)] = $this->redactSensitiveAuthTerms($child);
}
return $redacted;
}
if (is_string($value)) {
return $this->redactSensitiveAuthString($value);
}
return $value;
}
private function redactSensitiveAuthString(string $value): string
{
return str_ireplace(
['remember_token', 'two_factor', 'api_token', 'provider_token', 'password', 'secret'],
['auth_token_reference', 'mfa', 'api_credential', 'provider_credential', 'credential', 'sensitive_value'],
$value,
);
}
}
@@ -121,7 +121,7 @@ class ExamDraftTeacherService
'class_section_id' => $classSectionId,
'semester' => $semester,
'school_year' => $schoolYear,
'exam_type' => $examType !== '' ? $examType : null,
'exam_type' => $title,
'draft_title' => $title,
'status' => 'submitted',
'version' => $nextVersion,
@@ -8,6 +8,7 @@ use App\Models\User;
use App\Services\ApplicationUrlService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
class ExtraChargesChargeService
{
@@ -89,7 +90,7 @@ class ExtraChargesChargeService
$charge->amount = $newAmount;
$charge->due_date = $payload['due_date'] ?? $charge->due_date;
$charge->charge_type = $chargeType;
if (array_key_exists('updated_by', $payload)) {
if (array_key_exists('updated_by', $payload) && Schema::hasColumn('additional_charges', 'updated_by')) {
$charge->updated_by = $payload['updated_by'];
}
$charge->save();
@@ -222,6 +222,17 @@ class ParentRegistrationService
->where('parent_id', $parentId)
->firstOrFail();
$hasEnrollment = \Illuminate\Support\Facades\DB::table('enrollments')
->where('student_id', $studentId)
->exists();
$hasClassAssignment = \Illuminate\Support\Facades\DB::table('student_class')
->where('student_id', $studentId)
->exists();
if ($hasEnrollment || $hasClassAssignment) {
throw new \RuntimeException('Student deletion is not allowed after enrollment or class assignment.');
}
$student->delete();
$remaining = Student::query()->where('parent_id', $parentId)->count();
@@ -3,6 +3,7 @@
namespace App\Services\Payments;
use App\Models\PaymentTransaction;
use Illuminate\Support\Facades\Schema;
class PaymentTransactionService
{
@@ -22,7 +23,9 @@ class PaymentTransactionService
'semester' => $payload['semester'] ?? null,
];
return PaymentTransaction::query()->create($data);
$columns = array_flip(Schema::getColumnListing('payment_transactions'));
return PaymentTransaction::query()->create(array_intersect_key($data, $columns));
}
public function getByPayment(int $paymentId): array
@@ -4,6 +4,7 @@ namespace App\Services\Reimbursements;
use App\Models\Expense;
use App\Models\Reimbursement;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
class ReimbursementCrudService
@@ -87,8 +88,10 @@ class ReimbursementCrudService
'reimbursement_method' => $method,
'check_number' => $method === 'Check' ? ($payload['check_number'] ?? null) : null,
'receipt_path' => $receiptName,
'updated_by' => $userId ?: null,
];
if (Schema::hasColumn('reimbursements', 'updated_by')) {
$updateData['updated_by'] = $userId ?: null;
}
$reimb->update($updateData);
@@ -13,6 +13,7 @@ use App\Models\Student;
use App\Models\StudentClass;
use App\Models\TeacherClass;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class ReportCardService
{
@@ -105,8 +106,9 @@ class ReportCardService
$students = [];
try {
$classSectionNameSql = $this->classSectionNameAggregateSql();
$builder = DB::table('semester_scores as ss')
->select('s.id', 's.firstname', 's.lastname', DB::raw('GROUP_CONCAT(DISTINCT cs.class_section_name ORDER BY cs.class_section_name SEPARATOR ", ") AS class_section_name'))
->select('s.id', 's.firstname', 's.lastname', DB::raw($classSectionNameSql.' AS class_section_name'))
->join('students as s', 's.id', '=', 'ss.student_id')
->leftJoin('student_class as sc', function ($join) {
$join->on('sc.student_id', '=', 's.id')
@@ -133,7 +135,7 @@ class ReportCardService
}
if (empty($students)) {
$fallback = Student::query()
->select('students.id', 'students.firstname', 'students.lastname', DB::raw('GROUP_CONCAT(DISTINCT cs.class_section_name ORDER BY cs.class_section_name SEPARATOR ", ") AS class_section_name'))
->select('students.id', 'students.firstname', 'students.lastname', DB::raw($classSectionNameSql.' AS class_section_name'))
->leftJoin('student_class as sc', 'sc.student_id', '=', 'students.id')
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id');
if ($year !== '') {
@@ -643,6 +645,10 @@ class ReportCardService
];
foreach ($tables as [$table, $pk]) {
if (! Schema::hasTable($table)) {
continue;
}
$result = DB::table($table)
->select('class_section_name')
->where($pk, $classId)
@@ -1622,4 +1628,13 @@ class ReportCardService
return null;
}
private function classSectionNameAggregateSql(): string
{
if (DB::connection()->getDriverName() === 'sqlite') {
return 'GROUP_CONCAT(DISTINCT cs.class_section_name)';
}
return 'GROUP_CONCAT(DISTINCT cs.class_section_name ORDER BY cs.class_section_name SEPARATOR ", ")';
}
}
+12 -10
View File
@@ -24,7 +24,7 @@ class RoleCrudService
public function update(Role $role, array $payload): Role
{
$data = $this->normalizePayload($payload, $role->id, $role->slug ?? null);
$data = $this->normalizePayload($payload, $role);
try {
return DB::transaction(function () use ($role, $data) {
@@ -51,14 +51,16 @@ class RoleCrudService
}
}
private function normalizePayload(array $payload, ?int $ignoreId = null, ?string $currentSlug = null): array
private function normalizePayload(array $payload, ?Role $role = null): array
{
$rawName = trim((string) ($payload['name'] ?? ''));
$rawSlug = trim((string) ($payload['slug'] ?? ''));
$route = trim((string) ($payload['dashboard_route'] ?? ''));
$desc = trim((string) ($payload['description'] ?? ''));
$priority = (int) ($payload['priority'] ?? 0);
$isActive = ! empty($payload['is_active']) ? 1 : 0;
$rawName = trim((string) ($payload['name'] ?? $role?->name ?? ''));
$rawSlug = trim((string) ($payload['slug'] ?? $role?->slug ?? ''));
$route = trim((string) ($payload['dashboard_route'] ?? $role?->dashboard_route ?? ''));
$desc = trim((string) ($payload['description'] ?? $role?->description ?? ''));
$priority = (int) ($payload['priority'] ?? $role?->priority ?? 0);
$isActive = array_key_exists('is_active', $payload)
? (! empty($payload['is_active']) ? 1 : 0)
: (int) ($role?->is_active ?? 0);
$name = strtolower($rawName);
$dashboardRoute = strtolower($route);
@@ -68,8 +70,8 @@ class RoleCrudService
$slug = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $slug));
$slug = trim($slug, '_');
if ($slug !== $currentSlug) {
$slug = $this->ensureUniqueSlug($slug, $ignoreId);
if ($slug !== ($role?->slug ?? null)) {
$slug = $this->ensureUniqueSlug($slug, $role?->id);
}
return [
+2 -1
View File
@@ -7,6 +7,7 @@ use App\Models\User;
use App\Models\UserRole;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
@@ -172,7 +173,7 @@ class UserManagementService
'status' => $payload['status'] ?? 'Inactive',
'is_suspended' => isset($payload['is_suspended']) ? (bool) $payload['is_suspended'] : 0,
'failed_attempts' => $payload['failed_attempts'] ?? null,
'password' => pbkdf2_hash((string) ($payload['password'] ?? '')),
'password' => Hash::make((string) ($payload['password'] ?? '')),
'token' => $payload['token'] ?? null,
'account_id' => $payload['account_id'] ?? null,
'user_type' => $payload['user_type'] ?? 'primary',
@@ -12,27 +12,27 @@ class WhatsappContactService
public function listParentContacts(?string $schoolYear, ?string $semester): array
{
$schoolYear = trim((string) $schoolYear);
$semester = trim((string) $semester);
$schoolYear = $this->normalizeFilter($schoolYear);
$semester = $this->normalizeFilter($semester);
$builder = DB::table('parents as sp');
$builder->select('
sp.id AS parents_row_id,
sp.firstparent_id AS firstparent_id,
sp.secondparent_firstname AS sp_firstname,
sp.secondparent_lastname AS sp_lastname,
sp.secondparent_email AS sp_email,
sp.secondparent_phone AS sp_phone,
sp.school_year AS sp_school_year,
sp.semester AS sp_semester,
u.id AS u_id,
u.firstname AS u_firstname,
u.lastname AS u_lastname,
u.email AS u_email,
u.cellphone AS u_phone,
u.school_year AS u_school_year,
u.semester AS u_semester
');
$builder->select([
'sp.id AS parents_row_id',
'sp.firstparent_id AS firstparent_id',
'sp.secondparent_firstname AS sp_firstname',
'sp.secondparent_lastname AS sp_lastname',
'sp.secondparent_email AS sp_email',
'sp.secondparent_phone AS sp_phone',
'sp.school_year AS sp_school_year',
'sp.semester AS sp_semester',
'u.id AS u_id',
'u.firstname AS u_firstname',
'u.lastname AS u_lastname',
'u.email AS u_email',
'u.cellphone AS u_phone',
'u.school_year AS u_school_year',
'u.semester AS u_semester',
]);
$builder->leftJoin('users as u', 'u.id', '=', 'sp.firstparent_id');
if ($schoolYear !== '') {
@@ -98,8 +98,8 @@ class WhatsappContactService
public function listParentContactsByClass(?string $schoolYear, ?string $semester): array
{
$schoolYear = trim((string) $schoolYear);
$semester = trim((string) $semester);
$schoolYear = $this->normalizeFilter($schoolYear);
$semester = $this->normalizeFilter($semester);
$sections = DB::table('student_class as sc')
->select('sc.class_section_id', DB::raw('MAX(sc.school_year) AS school_year'))
@@ -263,4 +263,11 @@ class WhatsappContactService
return array_values($classSections);
}
private function normalizeFilter(?string $value): string
{
$value = trim((string) $value);
return in_array(strtolower($value), ['all', '*'], true) ? '' : $value;
}
}