update tests

This commit is contained in:
root
2026-06-08 22:06:30 -04:00
parent 79024235ef
commit 60ecacb7f8
54 changed files with 13243 additions and 5561 deletions
@@ -12,6 +12,7 @@ use App\Services\Auth\AuthSessionService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
/**
* Session (cookie) endpoints for SPA — JSON alongside JWT on /api/login.
@@ -141,12 +142,13 @@ class AuthSessionController extends Controller
], 403);
}
return response()->json([
'status' => true,
$loginPayload = $this->security->buildLoginResponse($fresh, JWTAuth::fromUser($fresh));
return response()->json(array_merge($loginPayload, [
'requires_role_selection' => ($dest['kind'] ?? '') === 'select_role',
'roles' => $dest['roles'] ?? null,
'next_url' => $dest['redirect_url'] ?? $this->urls->docsHomeUrl(false),
]);
]));
}
/** Closes the current web session. */
+33 -4
View File
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Models\BaseModel;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\SoftDeletes;
class Notification extends BaseModel
@@ -28,12 +29,40 @@ class Notification extends BaseModel
];
protected $casts = [
'scheduled_at' => 'datetime',
'expires_at' => 'datetime',
// If delivery_channels is JSON in DB:
'delivery_channels' => 'array',
'scheduled_at' => 'datetime',
'expires_at' => 'datetime',
];
/**
* MySQL stores channels as SET('in_app','email','sms'); expose as array in PHP.
*/
protected function deliveryChannels(): Attribute
{
return Attribute::make(
get: static function (?string $value): array {
if ($value === null || $value === '') {
return [];
}
$trimmed = trim($value);
if (str_starts_with($trimmed, '[')) {
$decoded = json_decode($trimmed, true);
return is_array($decoded) ? array_values($decoded) : [];
}
return array_values(array_filter(array_map('trim', explode(',', $value))));
},
set: static function ($value): string {
if (is_array($value)) {
return implode(',', array_values(array_filter(array_map('strval', $value))));
}
return (string) $value;
}
);
}
/**
* Equivalent of legacy getActiveNotifications()
* Active = scheduled_at <= now AND (expires_at is null OR expires_at > now)
+2 -2
View File
@@ -9,8 +9,8 @@ class PaymentTransaction extends BaseModel
{
protected $table = 'payment_transactions';
// ✅ legacy: useTimestamps = true (created_at/updated_at)
public $timestamps = true;
// Legacy table has no created_at/updated_at columns.
public $timestamps = false;
protected $fillable = [
'transaction_id',
+1 -1
View File
@@ -13,7 +13,7 @@ class StudentAllergy extends BaseModel
/**
* legacy: timestamps disabled
*/
public $timestamps = true;
public $timestamps = false;
protected $fillable = [
'student_id',
+1 -1
View File
@@ -13,7 +13,7 @@ class StudentMedicalCondition extends BaseModel
/**
* legacy: timestamps disabled
*/
public $timestamps = true;
public $timestamps = false;
protected $fillable = [
'student_id',
@@ -9,6 +9,7 @@ use App\Models\TeacherClass;
use Carbon\Carbon;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
class TeacherAttendanceSubmissionService
@@ -171,6 +172,20 @@ class TeacherAttendanceSubmissionService
$position = strtolower((string)($teacher['position'] ?? 'main'));
$position = $position === 'ta' ? 'ta' : 'main';
$staffPayload = [
'role_name' => 'Teacher',
'status' => strtolower((string)($teachersData[$teacherId]['status'] ?? 'present')),
'reason' => ($teachersData[$teacherId]['reason'] ?? null) ?: null,
'updated_at' => now(),
'created_at' => now(),
];
if (Schema::hasColumn('staff_attendance', 'class_section_id')) {
$staffPayload['class_section_id'] = $classSectionId;
}
if (Schema::hasColumn('staff_attendance', 'position')) {
$staffPayload['position'] = $position;
}
DB::table('staff_attendance')->updateOrInsert(
[
'user_id' => $teacherId,
@@ -178,15 +193,7 @@ class TeacherAttendanceSubmissionService
'semester' => $semester,
'school_year' => $schoolYear,
],
[
'role_name' => 'Teacher',
'class_section_id' => $classSectionId,
'position' => $position,
'status' => strtolower((string)($teachersData[$teacherId]['status'] ?? 'present')),
'reason' => ($teachersData[$teacherId]['reason'] ?? null) ?: null,
'updated_at' => now(),
'created_at' => now(),
]
$staffPayload
);
}
+10 -1
View File
@@ -129,7 +129,7 @@ class ApiLoginSecurityService
/**
* Top-level JSON body for API login success responses.
*
* @return array{status: true, token: string, user: array{id: int, name: string, roles: object, class_section_id: ?int, class_section_name: ?string}}
* @return array{status: true, token: string, access_token: string, token_type: string, expires_in: int, user: array{id: int, name: string, firstname: ?string, lastname: ?string, email: ?string, roles: object, class_section_id: ?int, class_section_name: ?string}}
*/
public function buildLoginResponse(User $user, string $jwtToken): array
{
@@ -139,10 +139,19 @@ class ApiLoginSecurityService
return [
'status' => true,
// Keep both names. Older clients read `token`; JWT-aware clients often read
// `access_token`. Removing either one just makes login fail in a new and
// irritatingly avoidable way.
'token' => $jwtToken,
'access_token' => $jwtToken,
'token_type' => 'bearer',
'expires_in' => (int) config('jwt.ttl') * 60,
'user' => [
'id' => (int) $user->id,
'name' => $fullName,
'firstname' => $user->firstname,
'lastname' => $user->lastname,
'email' => $user->email,
'roles' => $this->rolesToObjectMap($roleNames),
'class_section_id' => $teacherContext['class_section_id'],
'class_section_name' => $teacherContext['class_section_name'],
@@ -15,17 +15,17 @@ class ParentAttendanceReportCalendarService
*/
public function computeSundays(int $weeksBefore = 4, int $weeksAfter = 26): array
{
$today = new \DateTime('today');
$today = now()->startOfDay();
$dow = (int) $today->format('w');
$thisSunday = clone $today;
$thisSunday = $today->copy();
if ($dow !== 0) {
$thisSunday->modify('last sunday');
}
$default = clone $today;
$default = $today->copy();
if ($dow !== 0) {
$default->modify('next sunday');
$default = $default->copy()->modify('next sunday');
}
$schoolYear = (string) ($this->configService->context()['school_year'] ?? '');
@@ -33,13 +33,13 @@ class ParentAttendanceReportCalendarService
$dates = [];
for ($i = $weeksBefore; $i >= 1; $i--) {
$dates[] = (clone $thisSunday)->modify('-' . $i . ' week');
$dates[] = $thisSunday->copy()->modify('-' . $i . ' week');
}
$cursor = clone $thisSunday;
$cursor = $thisSunday->copy();
while ($cursor <= $cap) {
$dates[] = clone $cursor;
$cursor->modify('+1 week');
$dates[] = $cursor->copy();
$cursor = $cursor->copy()->modify('+1 week');
}
$rangeStart = reset($dates);
@@ -108,7 +108,7 @@ class ParentAttendanceReportCalendarService
public function firstSundayOfJune(string $schoolYear): \DateTime
{
$today = new \DateTime('today');
$today = now()->startOfDay();
$endYear = null;
if (preg_match('/^(\\d{4})\\D(\\d{4})$/', $schoolYear, $m)) {
$endYear = (int) $m[2];
@@ -118,10 +118,11 @@ class ParentAttendanceReportCalendarService
$endYear = ((int) $today->format('n') > 6) ? ($y + 1) : $y;
}
$juneFirst = new \DateTime(sprintf('%04d-06-01', $endYear));
$juneFirst = now()->parse(sprintf('%04d-06-01', $endYear))->startOfDay();
if ((int) $juneFirst->format('w') !== 0) {
$juneFirst->modify('next sunday');
$juneFirst = $juneFirst->copy()->modify('next sunday');
}
return $juneFirst;
}
}
@@ -82,6 +82,7 @@ class ParentRegistrationService
$emergencyContacts,
$schoolYear,
$semester,
$ageReference,
&$createdStudentIds,
&$skippedStudents
) {
@@ -16,7 +16,7 @@ class PaymentNotificationDispatchService
$notification = Notification::query()->create([
'title' => $title,
'message' => $message,
'target_group' => 'user',
'target_group' => 'everyone',
'delivery_channels' => $channels,
'priority' => 'normal',
'status' => 'sent',
+3 -1
View File
@@ -67,7 +67,9 @@ class SemesterRangeService
$lastDay = $this->configService->getLastSchoolDay();
try {
$target = new DateTimeImmutable($date ?: 'now');
$target = ($date !== null && $date !== '')
? new DateTimeImmutable($date)
: DateTimeImmutable::createFromInterface(now());
} catch (\Throwable) {
return '';
}
@@ -96,7 +96,7 @@ class TeacherDashboardService
->whereIn('tc.class_section_id', $classSectionIds)
->where('tc.school_year', $schoolYear)
->orderBy('tc.class_section_id')
->orderByRaw("FIELD(tc.position,'main','ta')")
->orderByRaw("CASE tc.position WHEN 'main' THEN 0 WHEN 'ta' THEN 1 ELSE 2 END")
->orderBy('u.firstname')
->get();