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
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:
@@ -60,6 +60,11 @@ class AttendanceTrackingController extends Controller
|
||||
|
||||
public function record(Request $request): JsonResponse
|
||||
{
|
||||
$guard = $this->forbidLowPrivilegeActor();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'student_id' => ['required', 'integer', 'min:1'],
|
||||
'date' => ['required', 'date_format:Y-m-d'],
|
||||
@@ -87,6 +92,11 @@ class AttendanceTrackingController extends Controller
|
||||
|
||||
public function sendAutoEmails(Request $request): JsonResponse
|
||||
{
|
||||
$guard = $this->forbidLowPrivilegeActor(adminOnly: true);
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$result = $this->service->sendAutoEmails($request->input('variant'));
|
||||
|
||||
return response()->json($result);
|
||||
@@ -124,6 +134,11 @@ class AttendanceTrackingController extends Controller
|
||||
|
||||
public function sendManualEmail(Request $request): JsonResponse
|
||||
{
|
||||
$guard = $this->forbidLowPrivilegeActor();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'student_id' => ['required', 'integer', 'min:1'],
|
||||
'to' => ['required', 'email'],
|
||||
@@ -173,6 +188,11 @@ class AttendanceTrackingController extends Controller
|
||||
|
||||
public function saveNotificationNote(Request $request): JsonResponse
|
||||
{
|
||||
$guard = $this->forbidLowPrivilegeActor();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'student_id' => ['required', 'integer', 'min:1'],
|
||||
'code' => ['required', 'string'],
|
||||
@@ -197,4 +217,33 @@ class AttendanceTrackingController extends Controller
|
||||
$result['status'] ?? 200
|
||||
);
|
||||
}
|
||||
|
||||
private function forbidLowPrivilegeActor(bool $adminOnly = false): ?JsonResponse
|
||||
{
|
||||
$user = request()->user() ?? auth()->user();
|
||||
if (! $user || ! method_exists($user, 'roles')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$roles = $user->roles()
|
||||
->pluck('roles.name')
|
||||
->map(fn ($role) => strtolower((string) $role))
|
||||
->all();
|
||||
|
||||
if ($roles === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$allowedRoles = $adminOnly
|
||||
? ['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal']
|
||||
: ['teacher', 'administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'];
|
||||
|
||||
foreach ($allowedRoles as $allowed) {
|
||||
if (in_array($allowed, $roles, true)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Forbidden.'], 403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,18 +166,32 @@ class AuthController extends BaseApiController
|
||||
|
||||
$teacherContext = $user->teacherSessionContext();
|
||||
|
||||
return $this->respondSuccess([
|
||||
$payload = [
|
||||
'id' => (int) $user->id,
|
||||
'firstname' => $user->firstname,
|
||||
'lastname' => $user->lastname,
|
||||
'email' => $user->email,
|
||||
'class_section_id' => $teacherContext['class_section_id'],
|
||||
'class_section_name' => $teacherContext['class_section_name'],
|
||||
], 'OK');
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'message' => 'OK',
|
||||
'data' => $payload,
|
||||
'user' => $payload,
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
if ($request->hasAny(['action', 'purge', 'rewrite', 'created_by', 'updated_by', 'deleted_by', 'actor_id'])) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'Unsupported logout payload.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$token = $request->bearerToken();
|
||||
if ($token && substr_count($token, '.') === 2) {
|
||||
try {
|
||||
|
||||
@@ -28,6 +28,7 @@ class RegisterController extends BaseApiController
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'captcha' => $captcha,
|
||||
'user' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class ClassPreparationController extends BaseApiController
|
||||
);
|
||||
|
||||
if (empty($payload['ok'])) {
|
||||
return $this->error('Unable to log class preparation print.', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
return $this->error('Unable to log class preparation print.', Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Services\ClassProgress\ClassProgressMetaService;
|
||||
use App\Services\ClassProgress\ClassProgressMutationService;
|
||||
use App\Services\ClassProgress\ClassProgressQueryService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -88,6 +89,10 @@ class ClassProgressController extends BaseApiController
|
||||
return $auth;
|
||||
}
|
||||
|
||||
if ($this->isLegacyTeacherProgressListRoute($request) && ! $this->hasAnyRole($auth, ['teacher', 'administrator', 'admin'])) {
|
||||
return $this->respondError('Forbidden.', Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
$filters = $request->validated();
|
||||
$meta = $this->queryService->meta($filters['school_year'] ?? null, $filters['semester'] ?? null);
|
||||
$filters['school_year'] = $filters['school_year'] ?? ($meta['school_year'] ?? null);
|
||||
@@ -251,6 +256,27 @@ class ClassProgressController extends BaseApiController
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function isLegacyTeacherProgressListRoute(ClassProgressIndexRequest $request): bool
|
||||
{
|
||||
return $request->is('api/v1/teacher/class-progress-history')
|
||||
|| $request->is('api/v1/teacher/class-progress-view');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roles
|
||||
*/
|
||||
private function hasAnyRole(User $user, array $roles): bool
|
||||
{
|
||||
$roles = array_map('strtolower', $roles);
|
||||
|
||||
return $user->roles()
|
||||
->where(function ($query) use ($roles) {
|
||||
$query->whereIn(DB::raw('LOWER(name)'), $roles)
|
||||
->orWhereIn(DB::raw('LOWER(slug)'), $roles);
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
|
||||
private function buildMetaPayload(?int $classId, int $sundayCount, ?string $schoolYear = null, ?string $semester = null): array
|
||||
{
|
||||
$meta = $this->queryService->meta($schoolYear, $semester);
|
||||
|
||||
@@ -54,9 +54,9 @@ class ChargeController extends BaseApiController
|
||||
return response()->json(['ok' => false, 'message' => 'Unable to create extra charge.'], 500);
|
||||
}
|
||||
|
||||
$status = ($result['ok'] ?? false)
|
||||
? (isset($result['error']) && $result['error'] === 'duplicate_charge' ? 409 : 201)
|
||||
: 422;
|
||||
$status = isset($result['error']) && $result['error'] === 'duplicate_charge'
|
||||
? 409
|
||||
: (($result['ok'] ?? false) ? 201 : 422);
|
||||
|
||||
return response()->json([
|
||||
'ok' => (bool) ($result['ok'] ?? false),
|
||||
@@ -82,9 +82,9 @@ class ChargeController extends BaseApiController
|
||||
return response()->json(['ok' => false, 'message' => 'Unable to create event charge.'], 500);
|
||||
}
|
||||
|
||||
$status = ($result['ok'] ?? false)
|
||||
? (isset($result['error']) && $result['error'] === 'duplicate_charge' ? 409 : 201)
|
||||
: 422;
|
||||
$status = isset($result['error']) && $result['error'] === 'duplicate_charge'
|
||||
? 409
|
||||
: (($result['ok'] ?? false) ? 201 : 422);
|
||||
|
||||
return response()->json([
|
||||
'ok' => (bool) ($result['ok'] ?? false),
|
||||
|
||||
@@ -24,7 +24,13 @@ class PaypalPaymentController extends BaseApiController
|
||||
|
||||
public function create(int $paymentId): JsonResponse
|
||||
{
|
||||
try {
|
||||
$result = $this->service->createPayment($paymentId);
|
||||
} catch (\RuntimeException $e) {
|
||||
$status = str_contains(strtolower($e->getMessage()), 'not found') ? 404 : 422;
|
||||
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], $status);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true] + $result);
|
||||
}
|
||||
@@ -32,11 +38,15 @@ class PaypalPaymentController extends BaseApiController
|
||||
public function execute(PaypalExecuteRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
try {
|
||||
$this->service->executePayment(
|
||||
(string) $payload['payer_id'],
|
||||
$payload['paypal_payment_id'] ?? null,
|
||||
isset($payload['payment_id']) ? (int) $payload['payment_id'] : null
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
@@ -104,6 +104,10 @@ class GradingController extends BaseApiController
|
||||
return $userId;
|
||||
}
|
||||
|
||||
if (! $this->currentUserHasAnyRole(['teacher', 'administrator', 'admin'])) {
|
||||
return response()->json(['ok' => false, 'message' => 'Forbidden.'], 403);
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
$locked = $this->lockService->toggle(
|
||||
(int) $payload['class_section_id'],
|
||||
@@ -122,6 +126,10 @@ class GradingController extends BaseApiController
|
||||
return $userId;
|
||||
}
|
||||
|
||||
if (! $this->currentUserHasAnyRole(['teacher', 'administrator', 'admin'])) {
|
||||
return response()->json(['ok' => false, 'message' => 'Forbidden.'], 403);
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
$count = $this->lockService->lockAll(
|
||||
(string) $payload['semester'],
|
||||
@@ -213,7 +221,11 @@ class GradingController extends BaseApiController
|
||||
|
||||
public function showPlacementBatch(int $batchId): JsonResponse
|
||||
{
|
||||
try {
|
||||
$data = $this->placementService->getPlacementBatch($batchId);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 404);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true] + $data);
|
||||
}
|
||||
@@ -226,12 +238,18 @@ class GradingController extends BaseApiController
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
try {
|
||||
$this->placementService->updatePlacementBatch(
|
||||
$batchId,
|
||||
(string) $payload['school_year'],
|
||||
$payload['placement_level'] ?? [],
|
||||
$userId
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
$status = str_contains(strtolower($e->getMessage()), 'not found') ? 404 : 422;
|
||||
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], $status);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
@@ -255,11 +273,15 @@ class GradingController extends BaseApiController
|
||||
public function belowSixtyEmail(BelowSixtyEmailRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
try {
|
||||
$data = $this->belowSixtyService->emailContext(
|
||||
(int) $payload['student_id'],
|
||||
(string) $payload['school_year'],
|
||||
(string) $payload['semester']
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true] + $data);
|
||||
}
|
||||
@@ -267,11 +289,15 @@ class GradingController extends BaseApiController
|
||||
public function sendBelowSixtyEmail(BelowSixtySendRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
try {
|
||||
$data = $this->belowSixtyService->emailContext(
|
||||
(int) $payload['student_id'],
|
||||
(string) $payload['school_year'],
|
||||
(string) $payload['semester']
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
||||
}
|
||||
|
||||
$subject = trim((string) ($payload['subject'] ?? ''));
|
||||
if ($subject !== '') {
|
||||
@@ -334,7 +360,11 @@ class GradingController extends BaseApiController
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
try {
|
||||
$count = $this->belowSixtyService->generateAllDecisions((string) $payload['school_year'], $userId);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true, 'saved_count' => $count]);
|
||||
}
|
||||
@@ -385,10 +415,14 @@ class GradingController extends BaseApiController
|
||||
public function belowSixtyDecisionEmail(BelowSixtyDecisionDetailsRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
try {
|
||||
$data = $this->belowSixtyService->decisionEmailContext(
|
||||
(int) $payload['student_id'],
|
||||
(string) $payload['school_year']
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true] + $data);
|
||||
}
|
||||
@@ -396,12 +430,16 @@ class GradingController extends BaseApiController
|
||||
public function sendBelowSixtyDecisionEmail(BelowSixtyDecisionSendRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
try {
|
||||
$this->belowSixtyService->sendDecisionEmail(
|
||||
(int) $payload['student_id'],
|
||||
(string) $payload['school_year'],
|
||||
isset($payload['subject']) ? (string) $payload['subject'] : null,
|
||||
isset($payload['html']) ? (string) $payload['html'] : null
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
@@ -428,4 +466,24 @@ class GradingController extends BaseApiController
|
||||
|
||||
return $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roles
|
||||
*/
|
||||
private function currentUserHasAnyRole(array $roles): bool
|
||||
{
|
||||
$user = request()->user() ?? auth()->user();
|
||||
if (! $user || ! method_exists($user, 'roles')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$roles = array_map('strtolower', $roles);
|
||||
|
||||
return $user->roles()
|
||||
->where(function ($query) use ($roles) {
|
||||
$query->whereIn('roles.name', $roles)
|
||||
->orWhereIn('roles.slug', $roles);
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,8 +94,7 @@ class AuthorizedUserInviteController extends Controller
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
'message' => 'Invalid credential setup request.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
@@ -112,7 +111,7 @@ class AuthorizedUserInviteController extends Controller
|
||||
return response()->json(['message' => $e->getMessage()], 400);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Password has been successfully set.']);
|
||||
return response()->json(['message' => 'Credential has been successfully set.']);
|
||||
}
|
||||
|
||||
private function isTrustedRedirectUrl(string $url): bool
|
||||
|
||||
@@ -33,7 +33,10 @@ class AuthorizedUsersController extends BaseApiController
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
return $this->success($rows);
|
||||
return $this->success([
|
||||
'data' => $rows,
|
||||
'users' => $rows,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(int $id): JsonResponse
|
||||
|
||||
@@ -142,11 +142,15 @@ class ParentController extends BaseApiController
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
try {
|
||||
$result = $this->registrationService->register(
|
||||
$guard,
|
||||
$payload['students'] ?? [],
|
||||
$payload['emergency_contacts'] ?? []
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
||||
}
|
||||
|
||||
$createdCount = count($result['created_student_ids']);
|
||||
$skipped = $result['skipped'] ?? [];
|
||||
@@ -177,7 +181,11 @@ class ParentController extends BaseApiController
|
||||
return $guard;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->registrationService->deleteStudent($guard, $studentId);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ class ScoreCommentController extends BaseApiController
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
try {
|
||||
$result = $this->service->save(
|
||||
(int) $payload['class_section_id'],
|
||||
(string) $payload['semester'],
|
||||
@@ -87,6 +88,9 @@ class ScoreCommentController extends BaseApiController
|
||||
$payload['missing_ok'] ?? [],
|
||||
$userId
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 409);
|
||||
}
|
||||
|
||||
if (! empty($result['errors'])) {
|
||||
return response()->json(['ok' => false, 'errors' => $result['errors']], 422);
|
||||
@@ -103,6 +107,7 @@ class ScoreCommentController extends BaseApiController
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
try {
|
||||
$result = $this->service->update(
|
||||
(int) $payload['class_section_id'],
|
||||
(string) $payload['semester'],
|
||||
@@ -111,6 +116,9 @@ class ScoreCommentController extends BaseApiController
|
||||
$payload['reviews'] ?? [],
|
||||
$userId
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 409);
|
||||
}
|
||||
|
||||
if (! empty($result['errors'])) {
|
||||
return response()->json(['ok' => false, 'errors' => $result['errors']], 422);
|
||||
|
||||
@@ -55,6 +55,10 @@ class ScoreController extends BaseApiController
|
||||
return $userId;
|
||||
}
|
||||
|
||||
if (! $this->currentUserHasAnyRole(['teacher', 'administrator', 'admin'])) {
|
||||
return response()->json(['ok' => false, 'message' => 'Forbidden.'], 403);
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
$this->service->submitScoresLock(
|
||||
(int) $payload['class_section_id'],
|
||||
@@ -90,4 +94,24 @@ class ScoreController extends BaseApiController
|
||||
|
||||
return $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roles
|
||||
*/
|
||||
private function currentUserHasAnyRole(array $roles): bool
|
||||
{
|
||||
$user = request()->user() ?? auth()->user();
|
||||
if (! $user || ! method_exists($user, 'roles')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$roles = array_map('strtolower', $roles);
|
||||
|
||||
return $user->roles()
|
||||
->where(function ($query) use ($roles) {
|
||||
$query->whereIn('roles.name', $roles)
|
||||
->orWhereIn('roles.slug', $roles);
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +404,18 @@ class StudentController extends BaseApiController
|
||||
$schoolYear = $validator->validated()['school_year'] ?? null;
|
||||
|
||||
$rows = StudentClass::query()
|
||||
->select('student_class.*', 'cs.class_section_name')
|
||||
->select(
|
||||
'student_class.id',
|
||||
'student_class.student_id',
|
||||
'student_class.class_section_id',
|
||||
'student_class.is_event_only',
|
||||
'student_class.semester',
|
||||
'student_class.school_year',
|
||||
'student_class.description',
|
||||
'student_class.created_at',
|
||||
'student_class.updated_at',
|
||||
'cs.class_section_name'
|
||||
)
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'student_class.class_section_id')
|
||||
->where('student_class.student_id', $studentId)
|
||||
->when($schoolYear !== null && $schoolYear !== '', fn ($q) => $q->where('student_class.school_year', $schoolYear))
|
||||
|
||||
@@ -13,6 +13,12 @@ class FamilyImportLegacyRequest extends ApiFormRequest
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
return [
|
||||
'file' => ['prohibited'],
|
||||
'attachment' => ['prohibited'],
|
||||
'document' => ['prohibited'],
|
||||
'import_file' => ['prohibited'],
|
||||
'avatar' => ['prohibited'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ class CarryforwardFinalizeRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'from_school_year' => ['required_without:id', 'string', 'max:20'],
|
||||
'to_school_year' => ['required_without:id', 'string', 'max:20'],
|
||||
'from_school_year' => ['required', 'string', 'max:20'],
|
||||
'to_school_year' => ['required', 'string', 'max:20'],
|
||||
'parent_id' => ['nullable', 'integer'],
|
||||
'reason' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
|
||||
@@ -13,6 +13,12 @@ class ParentEnrollmentActionRequest extends ApiFormRequest
|
||||
'enroll.*' => ['integer', 'min:1'],
|
||||
'withdraw' => ['nullable', 'array'],
|
||||
'withdraw.*' => ['integer', 'min:1'],
|
||||
'student_id' => ['prohibited'],
|
||||
'parent_id' => ['prohibited'],
|
||||
'class_section_id' => ['prohibited'],
|
||||
'previous_class_section_id' => ['prohibited'],
|
||||
'force' => ['prohibited'],
|
||||
'approved_by' => ['prohibited'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ class ParentStudentUpdateRequest extends ApiFormRequest
|
||||
'registration_grade' => ['nullable', 'string', 'max:50'],
|
||||
'allergies' => ['nullable', 'array'],
|
||||
'medical_conditions' => ['nullable', 'array'],
|
||||
'student_id' => ['prohibited'],
|
||||
'parent_id' => ['prohibited'],
|
||||
'class_section_id' => ['prohibited'],
|
||||
'previous_class_section_id' => ['prohibited'],
|
||||
'force' => ['prohibited'],
|
||||
'approved_by' => ['prohibited'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ class StoreAuthorizedUserRequest extends FormRequest
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'user_id' => ['prohibited'],
|
||||
'authorized_user_id' => ['prohibited'],
|
||||
'password' => ['prohibited'],
|
||||
'password_confirmation' => ['prohibited'],
|
||||
'password_confirm' => ['prohibited'],
|
||||
'current_password' => ['prohibited'],
|
||||
'role' => ['prohibited'],
|
||||
'role_id' => ['prohibited'],
|
||||
'roles' => ['prohibited'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,15 @@ class UpdateAuthorizedUserRequest extends FormRequest
|
||||
{
|
||||
return [
|
||||
'email' => ['sometimes', 'nullable', 'string', 'email'],
|
||||
'user_id' => ['prohibited'],
|
||||
'authorized_user_id' => ['prohibited'],
|
||||
'password' => ['prohibited'],
|
||||
'password_confirmation' => ['prohibited'],
|
||||
'password_confirm' => ['prohibited'],
|
||||
'current_password' => ['prohibited'],
|
||||
'role' => ['prohibited'],
|
||||
'role_id' => ['prohibited'],
|
||||
'roles' => ['prohibited'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ class ClassAttendanceStudentResource extends JsonResource
|
||||
'firstname' => $this->resource['firstname'] ?? null,
|
||||
'lastname' => $this->resource['lastname'] ?? null,
|
||||
'class_section_id' => $this->resource['class_section_id'] ?? null,
|
||||
'updated_by' => $this->resource['updated_by'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,8 +310,8 @@ class Invoice extends BaseModel
|
||||
|
||||
$affected = DB::update(
|
||||
"UPDATE invoices
|
||||
SET total_amount = GREATEST(total_amount - {$val}, 0),
|
||||
balance = GREATEST(balance - {$val}, 0)
|
||||
SET total_amount = CASE WHEN total_amount - {$val} < 0 THEN 0 ELSE total_amount - {$val} END,
|
||||
balance = CASE WHEN balance - {$val} < 0 THEN 0 ELSE balance - {$val} END
|
||||
WHERE id = ?",
|
||||
[$invoiceId]
|
||||
);
|
||||
@@ -326,8 +326,8 @@ class Invoice extends BaseModel
|
||||
$affected = DB::table('invoices')
|
||||
->where('id', $invoiceId)
|
||||
->update([
|
||||
'total_amount' => DB::raw('GREATEST(total_amount - '.$amount.', 0)'),
|
||||
'balance' => DB::raw('GREATEST(balance - '.$amount.', 0)'),
|
||||
'total_amount' => DB::raw('CASE WHEN total_amount - '.$amount.' < 0 THEN 0 ELSE total_amount - '.$amount.' END'),
|
||||
'balance' => DB::raw('CASE WHEN balance - '.$amount.' < 0 THEN 0 ELSE balance - '.$amount.' END'),
|
||||
]);
|
||||
|
||||
return $affected >= 0;
|
||||
|
||||
@@ -699,23 +699,32 @@ 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
|
||||
$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),
|
||||
{$scoreExpression},
|
||||
{$commentExpression},
|
||||
?,
|
||||
?,
|
||||
NOW(), NOW()
|
||||
FROM competition_scores cs
|
||||
JOIN competition_class_winners ccw
|
||||
?, ?
|
||||
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 = ?
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ", ")';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,4 +80,22 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
return response()->json(['message' => 'Unauthorized.'], 401);
|
||||
}
|
||||
});
|
||||
|
||||
$exceptions->render(function (\Illuminate\Auth\Access\AuthorizationException $e, Request $request) {
|
||||
if ($request->is('api/*') || $request->expectsJson()) {
|
||||
return response()->json(['message' => $e->getMessage() ?: 'Forbidden.'], 403);
|
||||
}
|
||||
});
|
||||
|
||||
$exceptions->render(function (\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e, Request $request) {
|
||||
if ($request->is('api/*') || $request->expectsJson()) {
|
||||
return response()->json(['message' => $e->getMessage() ?: 'Request failed.'], $e->getStatusCode());
|
||||
}
|
||||
});
|
||||
|
||||
$exceptions->render(function (\TypeError $e, Request $request) {
|
||||
if ($request->is('api/*') || $request->expectsJson()) {
|
||||
return response()->json(['message' => 'Resource not found.'], 404);
|
||||
}
|
||||
});
|
||||
})->create();
|
||||
|
||||
@@ -23,6 +23,7 @@ class ClassProgressReportFactory extends Factory
|
||||
'unit_title' => 'Unit 1 / Chapter A',
|
||||
'covered' => 'Lesson overview',
|
||||
'homework' => 'Practice reading',
|
||||
'next_week_plan' => 'Continue with the next lesson',
|
||||
'status' => 'on_track',
|
||||
'support_needed' => null,
|
||||
'flags_json' => ['flag'],
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('user_preferences') || Schema::hasColumn('user_preferences', 'timezone')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('user_preferences', function (Blueprint $table): void {
|
||||
$table->string('timezone', 80)->default('UTC')->after('language');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('user_preferences') || ! Schema::hasColumn('user_preferences', 'timezone')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('user_preferences', function (Blueprint $table): void {
|
||||
$table->dropColumn('timezone');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('communication_logs') || Schema::hasColumn('communication_logs', 'updated_at')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('communication_logs', function (Blueprint $table): void {
|
||||
$table->dateTime('updated_at')->nullable()->after('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('communication_logs') || ! Schema::hasColumn('communication_logs', 'updated_at')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('communication_logs', function (Blueprint $table): void {
|
||||
$table->dropColumn('updated_at');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
# Agent Instructions
|
||||
|
||||
Always communicate in English.
|
||||
|
||||
Use English for:
|
||||
- Responses
|
||||
- Code comments
|
||||
- Documentation
|
||||
- Commit messages
|
||||
- Pull request descriptions
|
||||
- Error analysis
|
||||
|
||||
Only use another language if explicitly requested.
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
<env name="JWT_SECRET" value="testing_jwt_secret_not_for_production"/>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value="/tmp/test_school_api.sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="DB_BUSY_TIMEOUT" value="5000"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+20
-20
@@ -1,28 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites>
|
||||
<testsuite name="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/phpunit.xml" tests="14" assertions="88" errors="0" failures="1" skipped="0" time="1.293065">
|
||||
<testsuite name="Feature" tests="14" assertions="88" errors="0" failures="1" skipped="0" time="1.293065">
|
||||
<testsuite name="Tests\Feature\Api\ApiAttendanceTemplateFeatureTest" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAttendanceTemplateFeatureTest.php" tests="3" assertions="22" errors="0" failures="0" skipped="0" time="0.631035">
|
||||
<testcase name="test_attendance_comment_templates_can_be_managed_through_current_api" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAttendanceTemplateFeatureTest.php" line="14" class="Tests\Feature\Api\ApiAttendanceTemplateFeatureTest" classname="Tests.Feature.Api.ApiAttendanceTemplateFeatureTest" assertions="12" time="0.590915"/>
|
||||
<testcase name="test_attendance_comment_template_validation_blocks_bad_ranges" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAttendanceTemplateFeatureTest.php" line="51" class="Tests\Feature\Api\ApiAttendanceTemplateFeatureTest" classname="Tests.Feature.Api.ApiAttendanceTemplateFeatureTest" assertions="4" time="0.019492"/>
|
||||
<testcase name="test_legacy_attendance_template_aliases_still_work" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAttendanceTemplateFeatureTest.php" line="64" class="Tests\Feature\Api\ApiAttendanceTemplateFeatureTest" classname="Tests.Feature.Api.ApiAttendanceTemplateFeatureTest" assertions="6" time="0.020628"/>
|
||||
<testsuite name="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/phpunit.xml" tests="14" assertions="88" errors="0" failures="1" skipped="0" time="0.936905">
|
||||
<testsuite name="Feature" tests="14" assertions="88" errors="0" failures="1" skipped="0" time="0.936905">
|
||||
<testsuite name="Tests\Feature\Api\ApiAttendanceTemplateFeatureTest" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAttendanceTemplateFeatureTest.php" tests="3" assertions="22" errors="0" failures="0" skipped="0" time="0.495418">
|
||||
<testcase name="test_attendance_comment_templates_can_be_managed_through_current_api" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAttendanceTemplateFeatureTest.php" line="14" class="Tests\Feature\Api\ApiAttendanceTemplateFeatureTest" classname="Tests.Feature.Api.ApiAttendanceTemplateFeatureTest" assertions="12" time="0.459266"/>
|
||||
<testcase name="test_attendance_comment_template_validation_blocks_bad_ranges" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAttendanceTemplateFeatureTest.php" line="51" class="Tests\Feature\Api\ApiAttendanceTemplateFeatureTest" classname="Tests.Feature.Api.ApiAttendanceTemplateFeatureTest" assertions="4" time="0.017761"/>
|
||||
<testcase name="test_legacy_attendance_template_aliases_still_work" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAttendanceTemplateFeatureTest.php" line="64" class="Tests\Feature\Api\ApiAttendanceTemplateFeatureTest" classname="Tests.Feature.Api.ApiAttendanceTemplateFeatureTest" assertions="6" time="0.018392"/>
|
||||
</testsuite>
|
||||
<testsuite name="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" tests="6" assertions="38" errors="0" failures="0" skipped="0" time="0.532192">
|
||||
<testcase name="test_api_login_returns_token_and_user_object" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" line="16" class="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" classname="Tests.Feature.Api.ApiAuthenticationAndAuthorizationTest" assertions="14" time="0.046767"/>
|
||||
<testcase name="test_session_login_also_returns_token_and_user_object_for_spa_clients" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" line="40" class="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" classname="Tests.Feature.Api.ApiAuthenticationAndAuthorizationTest" assertions="13" time="0.033410"/>
|
||||
<testcase name="test_auth_me_requires_authentication" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" line="63" class="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" classname="Tests.Feature.Api.ApiAuthenticationAndAuthorizationTest" assertions="1" time="0.016178"/>
|
||||
<testcase name="test_auth_me_returns_the_current_authenticated_user" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" line="68" class="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" classname="Tests.Feature.Api.ApiAuthenticationAndAuthorizationTest" assertions="3" time="0.018364"/>
|
||||
<testcase name="test_admin_only_routes_reject_non_admin_users" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" line="83" class="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" classname="Tests.Feature.Api.ApiAuthenticationAndAuthorizationTest" assertions="6" time="0.045925"/>
|
||||
<testcase name="test_protected_api_routes_reject_anonymous_requests_before_business_logic" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" line="99" class="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" classname="Tests.Feature.Api.ApiAuthenticationAndAuthorizationTest" assertions="1" time="0.371549"/>
|
||||
<testsuite name="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" tests="6" assertions="38" errors="0" failures="0" skipped="0" time="0.327981">
|
||||
<testcase name="test_api_login_returns_token_and_user_object" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" line="16" class="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" classname="Tests.Feature.Api.ApiAuthenticationAndAuthorizationTest" assertions="14" time="0.044219"/>
|
||||
<testcase name="test_session_login_also_returns_token_and_user_object_for_spa_clients" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" line="40" class="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" classname="Tests.Feature.Api.ApiAuthenticationAndAuthorizationTest" assertions="13" time="0.032755"/>
|
||||
<testcase name="test_auth_me_requires_authentication" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" line="63" class="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" classname="Tests.Feature.Api.ApiAuthenticationAndAuthorizationTest" assertions="1" time="0.014267"/>
|
||||
<testcase name="test_auth_me_returns_the_current_authenticated_user" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" line="68" class="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" classname="Tests.Feature.Api.ApiAuthenticationAndAuthorizationTest" assertions="3" time="0.014557"/>
|
||||
<testcase name="test_admin_only_routes_reject_non_admin_users" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" line="83" class="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" classname="Tests.Feature.Api.ApiAuthenticationAndAuthorizationTest" assertions="6" time="0.026174"/>
|
||||
<testcase name="test_protected_api_routes_reject_anonymous_requests_before_business_logic" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php" line="99" class="Tests\Feature\Api\ApiAuthenticationAndAuthorizationTest" classname="Tests.Feature.Api.ApiAuthenticationAndAuthorizationTest" assertions="1" time="0.196009"/>
|
||||
</testsuite>
|
||||
<testsuite name="Tests\Feature\Api\ApiPreferencesFeatureTest" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPreferencesFeatureTest.php" tests="4" assertions="27" errors="0" failures="0" skipped="0" time="0.092580">
|
||||
<testcase name="test_authenticated_user_can_create_read_update_and_delete_preferences" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPreferencesFeatureTest.php" line="14" class="Tests\Feature\Api\ApiPreferencesFeatureTest" classname="Tests.Feature.Api.ApiPreferencesFeatureTest" assertions="12" time="0.025075"/>
|
||||
<testcase name="test_non_admin_cannot_list_or_manage_another_users_preferences" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPreferencesFeatureTest.php" line="50" class="Tests\Feature\Api\ApiPreferencesFeatureTest" classname="Tests.Feature.Api.ApiPreferencesFeatureTest" assertions="6" time="0.025534"/>
|
||||
<testcase name="test_admin_can_list_and_delete_any_users_preferences" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPreferencesFeatureTest.php" line="82" class="Tests\Feature\Api\ApiPreferencesFeatureTest" classname="Tests.Feature.Api.ApiPreferencesFeatureTest" assertions="5" time="0.022858"/>
|
||||
<testcase name="test_preferences_validation_rejects_invalid_options" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPreferencesFeatureTest.php" line="103" class="Tests\Feature\Api\ApiPreferencesFeatureTest" classname="Tests.Feature.Api.ApiPreferencesFeatureTest" assertions="4" time="0.019113"/>
|
||||
<testsuite name="Tests\Feature\Api\ApiPreferencesFeatureTest" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPreferencesFeatureTest.php" tests="4" assertions="27" errors="0" failures="0" skipped="0" time="0.082273">
|
||||
<testcase name="test_authenticated_user_can_create_read_update_and_delete_preferences" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPreferencesFeatureTest.php" line="14" class="Tests\Feature\Api\ApiPreferencesFeatureTest" classname="Tests.Feature.Api.ApiPreferencesFeatureTest" assertions="12" time="0.023361"/>
|
||||
<testcase name="test_non_admin_cannot_list_or_manage_another_users_preferences" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPreferencesFeatureTest.php" line="50" class="Tests\Feature\Api\ApiPreferencesFeatureTest" classname="Tests.Feature.Api.ApiPreferencesFeatureTest" assertions="6" time="0.022090"/>
|
||||
<testcase name="test_admin_can_list_and_delete_any_users_preferences" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPreferencesFeatureTest.php" line="82" class="Tests\Feature\Api\ApiPreferencesFeatureTest" classname="Tests.Feature.Api.ApiPreferencesFeatureTest" assertions="5" time="0.020525"/>
|
||||
<testcase name="test_preferences_validation_rejects_invalid_options" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPreferencesFeatureTest.php" line="103" class="Tests\Feature\Api\ApiPreferencesFeatureTest" classname="Tests.Feature.Api.ApiPreferencesFeatureTest" assertions="4" time="0.016297"/>
|
||||
</testsuite>
|
||||
<testsuite name="Tests\Feature\Api\ApiPublicEndpointsTest" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPublicEndpointsTest.php" tests="1" assertions="1" errors="0" failures="1" skipped="0" time="0.037258">
|
||||
<testcase name="test_health_endpoint_is_available" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPublicEndpointsTest.php" line="12" class="Tests\Feature\Api\ApiPublicEndpointsTest" classname="Tests.Feature.Api.ApiPublicEndpointsTest" assertions="1" time="0.037258">
|
||||
<testsuite name="Tests\Feature\Api\ApiPublicEndpointsTest" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPublicEndpointsTest.php" tests="1" assertions="1" errors="0" failures="1" skipped="0" time="0.031232">
|
||||
<testcase name="test_health_endpoint_is_available" file="/Volumes/ExternalApps/repos/alrahma_sunday_school_api/tests/Feature/Api/ApiPublicEndpointsTest.php" line="12" class="Tests\Feature\Api\ApiPublicEndpointsTest" classname="Tests.Feature.Api.ApiPublicEndpointsTest" assertions="1" time="0.031232">
|
||||
<failure type="PHPUnit\Framework\ExpectationFailedException">Tests\Feature\Api\ApiPublicEndpointsTest::test_health_endpoint_is_available
|
||||
Expected response status code [200] but received 503.
|
||||
Failed asserting that 503 is identical to 200.
|
||||
|
||||
+60
-43
@@ -181,17 +181,19 @@ Route::post('set_authorized_user_password/{authorizedUserId}', [AuthorizedUserIn
|
||||
|
||||
/*
|
||||
| Legacy paths without /v1: POST cannot use 301 reliably; alias to same handlers as v1 (auth:api).
|
||||
| GET uses 301 to canonical /api/v1/...
|
||||
| GET aliases return JSON behind auth so browser cookies do not grant API access.
|
||||
*/
|
||||
Route::middleware('auth:api')->prefix('attendance-templates')->group(function () {
|
||||
Route::post('save', [AttendanceCommentTemplateController::class, 'legacySave']);
|
||||
Route::post('delete', [AttendanceCommentTemplateController::class, 'legacyDelete']);
|
||||
});
|
||||
|
||||
Route::permanentRedirect('attendance-templates', '/api/v1/attendance-templates');
|
||||
Route::permanentRedirect('attendance-comment-templates', '/api/v1/attendance-comment-templates');
|
||||
Route::permanentRedirect('attendance-comment-templates/list-data', '/api/v1/attendance-comment-templates/list-data');
|
||||
Route::permanentRedirect('administrator/attendance-templates', '/api/v1/administrator/attendance-templates');
|
||||
Route::middleware(['auth:api', 'throttle:60,1'])->group(function () {
|
||||
Route::get('attendance-templates', [AttendanceCommentTemplateController::class, 'listData']);
|
||||
Route::get('attendance-comment-templates', [AttendanceCommentTemplateController::class, 'index']);
|
||||
Route::get('attendance-comment-templates/list-data', [AttendanceCommentTemplateController::class, 'listData']);
|
||||
Route::get('administrator/attendance-templates', [AttendanceCommentTemplateController::class, 'bootstrapUrls']);
|
||||
});
|
||||
|
||||
Route::prefix('v1')->group(function () {
|
||||
// Public auth aliases without the `/auth` prefix.
|
||||
@@ -246,16 +248,16 @@ Route::prefix('v1')->group(function () {
|
||||
Route::get('/', [PreferencesController::class, 'show']);
|
||||
Route::post('/', [PreferencesController::class, 'store']);
|
||||
Route::get('list', [PreferencesController::class, 'index']);
|
||||
Route::get('{userId}', [PreferencesController::class, 'showForUser']);
|
||||
Route::put('{userId}', [PreferencesController::class, 'updateForUser']);
|
||||
Route::delete('{userId}', [PreferencesController::class, 'destroy']);
|
||||
Route::get('{userId}', [PreferencesController::class, 'showForUser'])->whereNumber('userId');
|
||||
Route::put('{userId}', [PreferencesController::class, 'updateForUser'])->whereNumber('userId');
|
||||
Route::delete('{userId}', [PreferencesController::class, 'destroy'])->whereNumber('userId');
|
||||
});
|
||||
|
||||
Route::middleware('multi.auth')->prefix('nav-builder')->group(function () {
|
||||
Route::get('menu', [NavBuilderController::class, 'menu']);
|
||||
Route::get('data', [NavBuilderController::class, 'data']);
|
||||
Route::post('/', [NavBuilderController::class, 'store']);
|
||||
Route::delete('{id}', [NavBuilderController::class, 'destroy']);
|
||||
Route::delete('{id}', [NavBuilderController::class, 'destroy'])->whereNumber('id');
|
||||
Route::post('reorder', [NavBuilderController::class, 'reorder']);
|
||||
});
|
||||
|
||||
@@ -452,16 +454,16 @@ Route::prefix('v1')->group(function () {
|
||||
Route::middleware('admin.access')->prefix('role-permission')->group(function () {
|
||||
Route::get('roles', [RolePermissionController::class, 'roles']);
|
||||
Route::post('roles', [RolePermissionController::class, 'storeRole']);
|
||||
Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole']);
|
||||
Route::patch('roles/{roleId}', [RolePermissionController::class, 'updateRole']);
|
||||
Route::delete('roles/{roleId}', [RolePermissionController::class, 'deleteRole']);
|
||||
Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole'])->whereNumber('roleId');
|
||||
Route::patch('roles/{roleId}', [RolePermissionController::class, 'updateRole'])->whereNumber('roleId');
|
||||
Route::delete('roles/{roleId}', [RolePermissionController::class, 'deleteRole'])->whereNumber('roleId');
|
||||
|
||||
Route::get('users', [RolePermissionController::class, 'users']);
|
||||
Route::post('users/{userId}/roles', [RolePermissionController::class, 'assignRoles']);
|
||||
Route::post('users/{userId}/roles', [RolePermissionController::class, 'assignRoles'])->whereNumber('userId');
|
||||
|
||||
Route::get('permissions', [RolePermissionController::class, 'permissions']);
|
||||
Route::post('permissions', [RolePermissionController::class, 'storePermission']);
|
||||
Route::get('permissions/{permissionId}', [RolePermissionController::class, 'showPermission']);
|
||||
Route::get('permissions/{permissionId}', [RolePermissionController::class, 'showPermission'])->whereNumber('permissionId');
|
||||
Route::patch('permissions/{permissionId}', [RolePermissionController::class, 'updatePermission']);
|
||||
Route::delete('permissions/{permissionId}', [RolePermissionController::class, 'deletePermission']);
|
||||
|
||||
@@ -485,7 +487,7 @@ Route::prefix('v1')->group(function () {
|
||||
Route::get('{schoolYear}/promotion-preview', [SchoolYearController::class, 'promotionPreview'])->whereNumber('schoolYear');
|
||||
});
|
||||
|
||||
Route::middleware(['auth:api', 'school_year.editable'])->prefix('attendance')->group(function () {
|
||||
Route::middleware(['multi.auth', 'school_year.editable'])->prefix('attendance')->group(function () {
|
||||
// Teacher
|
||||
Route::get('/teacher/grid', [TeacherAttendanceApiController::class, 'grid']);
|
||||
Route::get('/teacher/form', [TeacherAttendanceApiController::class, 'form']);
|
||||
@@ -498,8 +500,8 @@ Route::prefix('v1')->group(function () {
|
||||
|
||||
// Staff/admin monthly/admin attendance
|
||||
Route::get('/staff/month', [StaffAttendanceApiController::class, 'monthData']);
|
||||
Route::get('/staff/admins', [StaffAttendanceApiController::class, 'admins']);
|
||||
Route::post('/staff/admins/save', [StaffAttendanceApiController::class, 'saveAdmins']);
|
||||
Route::get('/staff/admins', [StaffAttendanceApiController::class, 'admins'])->middleware('admin.access');
|
||||
Route::post('/staff/admins/save', [StaffAttendanceApiController::class, 'saveAdmins'])->middleware('admin.access');
|
||||
Route::post('/staff/cell', [StaffAttendanceApiController::class, 'saveCell']);
|
||||
Route::get('/staff/month-csv', [StaffAttendanceApiController::class, 'monthCsv']);
|
||||
|
||||
@@ -527,7 +529,7 @@ Route::prefix('v1')->group(function () {
|
||||
});
|
||||
});
|
||||
|
||||
Route::middleware(app()->runningUnitTests() ? ['school_year.editable'] : ['multi.auth', 'school_year.editable'])->prefix('attendance-tracking')->group(function () {
|
||||
Route::middleware(app()->runningUnitTests() ? ['throttle:120,1', 'school_year.editable'] : ['multi.auth', 'school_year.editable'])->prefix('attendance-tracking')->group(function () {
|
||||
Route::get('/pending-violations', [AttendanceTrackingController::class, 'pendingViolations']);
|
||||
Route::get('/notified-violations', [AttendanceTrackingController::class, 'notifiedViolations']);
|
||||
Route::get('/student-case/{studentId}', [AttendanceTrackingController::class, 'studentCase']);
|
||||
@@ -706,12 +708,12 @@ Route::prefix('v1')->group(function () {
|
||||
Route::middleware('admin.access')->prefix('role-permissions')->group(function () {
|
||||
Route::get('roles', [RolePermissionController::class, 'roles']);
|
||||
Route::post('roles', [RolePermissionController::class, 'storeRole']);
|
||||
Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole']);
|
||||
Route::patch('roles/{roleId}', [RolePermissionController::class, 'updateRole']);
|
||||
Route::delete('roles/{roleId}', [RolePermissionController::class, 'deleteRole']);
|
||||
Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole'])->whereNumber('roleId');
|
||||
Route::patch('roles/{roleId}', [RolePermissionController::class, 'updateRole'])->whereNumber('roleId');
|
||||
Route::delete('roles/{roleId}', [RolePermissionController::class, 'deleteRole'])->whereNumber('roleId');
|
||||
|
||||
Route::get('users', [RolePermissionController::class, 'users']);
|
||||
Route::post('users/{userId}/roles', [RolePermissionController::class, 'assignRoles']);
|
||||
Route::post('users/{userId}/roles', [RolePermissionController::class, 'assignRoles'])->whereNumber('userId');
|
||||
|
||||
Route::get('permissions', [RolePermissionController::class, 'permissions']);
|
||||
Route::post('permissions', [RolePermissionController::class, 'storePermission']);
|
||||
@@ -727,7 +729,7 @@ Route::prefix('v1')->group(function () {
|
||||
Route::get('/', [RoleSwitcherController::class, 'index']);
|
||||
Route::post('switch', [RoleSwitcherController::class, 'switch']);
|
||||
});
|
||||
Route::prefix('settings/school-calendar')->group(function () {
|
||||
Route::middleware('admin.access')->prefix('settings/school-calendar')->group(function () {
|
||||
Route::get('options', [SchoolCalendarController::class, 'options']);
|
||||
Route::get('events', [SchoolCalendarController::class, 'index']);
|
||||
Route::get('events/{eventId}', [SchoolCalendarController::class, 'show']);
|
||||
@@ -736,7 +738,7 @@ Route::prefix('v1')->group(function () {
|
||||
Route::delete('events/{eventId}', [SchoolCalendarController::class, 'destroy']);
|
||||
});
|
||||
|
||||
Route::prefix('settings/events')->group(function () {
|
||||
Route::middleware('admin.access')->prefix('settings/events')->group(function () {
|
||||
Route::get('/', [EventController::class, 'index']);
|
||||
Route::post('/', [EventController::class, 'store']);
|
||||
Route::get('charges/list', [EventController::class, 'charges']);
|
||||
@@ -768,32 +770,38 @@ Route::prefix('v1')->group(function () {
|
||||
|
||||
Route::prefix('students')->group(function () {
|
||||
Route::get('/', [StudentApiController::class, 'index']);
|
||||
Route::post('/', [StudentApiController::class, 'store']);
|
||||
Route::get('score-card/selectable', [StudentApiController::class, 'scoreCardSelectable']);
|
||||
Route::get('assignments', [StudentApiController::class, 'assignments']);
|
||||
Route::get('removed', [StudentApiController::class, 'removed']);
|
||||
Route::get('promotion-totals', [StudentApiController::class, 'promotionTotals']);
|
||||
Route::middleware('admin.access')->group(function () {
|
||||
Route::post('/', [StudentApiController::class, 'store']);
|
||||
Route::post('assign-class', [StudentApiController::class, 'assignClass']);
|
||||
Route::post('remove-class', [StudentApiController::class, 'removeClass']);
|
||||
Route::get('removed', [StudentApiController::class, 'removed']);
|
||||
Route::post('set-active', [StudentApiController::class, 'setActive']);
|
||||
Route::post('auto-distribute', [StudentApiController::class, 'autoDistribute']);
|
||||
Route::get('promotion-totals', [StudentApiController::class, 'promotionTotals']);
|
||||
Route::patch('{studentId}', [StudentApiController::class, 'update']);
|
||||
Route::delete('{studentId}', [StudentApiController::class, 'destroy']);
|
||||
});
|
||||
Route::get('{studentId}', [StudentApiController::class, 'show']);
|
||||
Route::get('{studentId}/classes', [StudentApiController::class, 'classes']);
|
||||
Route::middleware('admin.access')->group(function () {
|
||||
Route::post('{studentId}/classes', [StudentApiController::class, 'assignClassForStudent']);
|
||||
Route::delete('{studentId}/classes/{classSectionId}', [StudentApiController::class, 'removeClassForStudent']);
|
||||
Route::post('{studentId}/promote', [StudentApiController::class, 'promote']);
|
||||
});
|
||||
Route::get('{studentId}/attendance', [StudentApiController::class, 'attendance']);
|
||||
Route::get('{studentId}/incidents', [StudentApiController::class, 'incidents']);
|
||||
Route::get('{studentId}/scores', [StudentApiController::class, 'scores']);
|
||||
Route::get('{studentId}/notes', [StudentApiController::class, 'notes']);
|
||||
Route::get('{studentId}/parents', [StudentApiController::class, 'parents']);
|
||||
Route::get('{studentId}/emergency-contacts', [StudentApiController::class, 'emergencyContacts']);
|
||||
Route::middleware('admin.access')->group(function () {
|
||||
Route::post('{studentId}/emergency-contacts', [StudentApiController::class, 'addEmergencyContact']);
|
||||
Route::patch('{studentId}/emergency-contacts/{contactId}', [StudentApiController::class, 'updateEmergencyContact']);
|
||||
Route::post('{studentId}/badge_scan', [StudentApiController::class, 'updateBadgeScan']);
|
||||
Route::post('{studentId}/photo', [StudentApiController::class, 'uploadPhoto']);
|
||||
});
|
||||
Route::get('{studentId}/photo', [StudentApiController::class, 'photo']);
|
||||
Route::get('{studentId}/report-card', [ReportCardsController::class, 'studentReport'])
|
||||
->name('api.v1.students.report-card');
|
||||
@@ -841,12 +849,12 @@ Route::prefix('v1')->group(function () {
|
||||
Route::get('/', [UserController::class, 'index']);
|
||||
Route::post('/', [UserController::class, 'store']);
|
||||
Route::get('login-activity', [UserController::class, 'loginActivity']);
|
||||
Route::get('{userId}', [UserController::class, 'show']);
|
||||
Route::put('{userId}', [UserController::class, 'update']);
|
||||
Route::delete('{userId}', [UserController::class, 'destroy']);
|
||||
Route::get('{userId}', [UserController::class, 'show'])->whereNumber('userId');
|
||||
Route::put('{userId}', [UserController::class, 'update'])->whereNumber('userId');
|
||||
Route::delete('{userId}', [UserController::class, 'destroy'])->whereNumber('userId');
|
||||
});
|
||||
|
||||
Route::prefix('administrator')->group(function () {
|
||||
Route::middleware('admin.access')->prefix('administrator')->group(function () {
|
||||
Route::get('login-activity', [UserController::class, 'loginActivity']);
|
||||
});
|
||||
|
||||
@@ -874,7 +882,7 @@ Route::prefix('v1')->group(function () {
|
||||
Route::post('send', [CommunicationController::class, 'send']);
|
||||
});
|
||||
|
||||
Route::prefix('inventory')->group(function () {
|
||||
Route::middleware('admin.access')->prefix('inventory')->group(function () {
|
||||
Route::get('items', [InventoryController::class, 'index']);
|
||||
Route::get('items/{id}', [InventoryController::class, 'show']);
|
||||
Route::post('items', [InventoryController::class, 'store']);
|
||||
@@ -962,7 +970,7 @@ Route::prefix('v1')->group(function () {
|
||||
Route::post('{id}/export-quiz', [CompetitionWinnersController::class, 'exportQuiz']);
|
||||
});
|
||||
|
||||
Route::prefix('discounts')->group(function () {
|
||||
Route::middleware('admin.access')->prefix('discounts')->group(function () {
|
||||
Route::get('/', [DiscountController::class, 'options']);
|
||||
Route::get('options', [DiscountController::class, 'options']);
|
||||
Route::post('apply', [DiscountController::class, 'apply']);
|
||||
@@ -989,10 +997,12 @@ Route::prefix('v1')->group(function () {
|
||||
Route::get('stakeholder-analysis/csv', [FinancialController::class, 'stakeholderAnalysisCsv']);
|
||||
Route::get('parent-payment-followups', [FinancialController::class, 'parentPaymentFollowups']);
|
||||
Route::get('parent-payment-followups/csv', [FinancialController::class, 'parentPaymentFollowupsCsv']);
|
||||
Route::middleware('admin.access')->group(function () {
|
||||
Route::post('parent-payment-followups/{parent}/note', [FinancialController::class, 'storeParentFollowUpNote'])->whereNumber('parent');
|
||||
Route::post('parent-payment-followups/{parent}/mark-contacted', [FinancialController::class, 'markParentContacted'])->whereNumber('parent');
|
||||
Route::post('parent-payment-followups/{parent}/promise-to-pay', [FinancialController::class, 'storePromiseToPay'])->whereNumber('parent');
|
||||
Route::post('parent-payment-followups/{parent}/resolve', [FinancialController::class, 'resolveParentFollowUp'])->whereNumber('parent');
|
||||
});
|
||||
|
||||
Route::get('carryforwards/preview', [BalanceCarryforwardController::class, 'preview']);
|
||||
Route::post('carryforwards/draft', [BalanceCarryforwardController::class, 'storeDraft']);
|
||||
@@ -1003,7 +1013,8 @@ Route::prefix('v1')->group(function () {
|
||||
Route::get('carryforwards/report', [BalanceCarryforwardController::class, 'report']);
|
||||
Route::get('carryforwards/report/csv', [BalanceCarryforwardController::class, 'reportCsv']);
|
||||
|
||||
Route::apiResource('event-charges', EventChargeController::class);
|
||||
Route::apiResource('event-charges', EventChargeController::class)
|
||||
->names('finance.event-charges');
|
||||
Route::post('event-charges/{eventCharge}/approve', [EventChargeController::class, 'approve'])->whereNumber('eventCharge');
|
||||
Route::post('event-charges/{eventCharge}/void', [EventChargeController::class, 'void'])->whereNumber('eventCharge');
|
||||
Route::post('event-charges/{eventCharge}/attach-to-invoice', [EventChargeController::class, 'attachToInvoice'])->whereNumber('eventCharge');
|
||||
@@ -1017,12 +1028,14 @@ Route::prefix('v1')->group(function () {
|
||||
Route::get('installments/due', [InstallmentPlanController::class, 'due']);
|
||||
Route::get('installments/overdue', [InstallmentPlanController::class, 'overdue']);
|
||||
|
||||
Route::middleware('admin.access')->group(function () {
|
||||
Route::post('payments/{payment}/send-receipt', [FinanceNotificationController::class, 'sendPaymentReceipt'])->whereNumber('payment');
|
||||
Route::post('refunds/{refund}/send-receipt', [FinanceNotificationController::class, 'sendRefundReceipt'])->whereNumber('refund');
|
||||
Route::post('invoices/{invoice}/send-statement', [FinanceNotificationController::class, 'sendInvoiceStatement'])->whereNumber('invoice');
|
||||
Route::post('parents/{parent}/send-overdue-reminder', [FinanceNotificationController::class, 'sendOverdueReminder'])->whereNumber('parent');
|
||||
Route::post('installments/{installment}/send-reminder', [FinanceNotificationController::class, 'sendInstallmentReminder'])->whereNumber('installment');
|
||||
Route::get('notification-logs', [FinanceNotificationController::class, 'logs']);
|
||||
});
|
||||
Route::get('notification-logs', [FinanceNotificationController::class, 'logs'])->middleware('admin.access');
|
||||
Route::get('financial-report/csv', [FinancialController::class, 'downloadCsv']);
|
||||
Route::get('financial-report/pdf', [FinancialController::class, 'downloadPdf']);
|
||||
Route::get('unpaid-parents', [FinancialController::class, 'unpaidParents']);
|
||||
@@ -1042,17 +1055,19 @@ Route::prefix('v1')->group(function () {
|
||||
});
|
||||
|
||||
Route::prefix('refunds')->group(function () {
|
||||
Route::post('recalculate-overpayments', [RefundController::class, 'recalculateOverpayments']);
|
||||
Route::get('parent-balances/{parentId}', [RefundController::class, 'parentBalances']);
|
||||
Route::get('/', [RefundController::class, 'index']);
|
||||
Route::post('/', [RefundController::class, 'store']);
|
||||
Route::get('{refundId}', [RefundController::class, 'show']);
|
||||
Route::middleware('admin.access')->group(function () {
|
||||
Route::post('recalculate-overpayments', [RefundController::class, 'recalculateOverpayments']);
|
||||
Route::post('/', [RefundController::class, 'store']);
|
||||
Route::patch('{refundId}', [RefundController::class, 'update']);
|
||||
Route::delete('{refundId}', [RefundController::class, 'destroy']);
|
||||
Route::post('{refundId}/approve', [RefundController::class, 'approve']);
|
||||
Route::post('{refundId}/reject', [RefundController::class, 'reject']);
|
||||
Route::post('{refundId}/payments', [RefundController::class, 'recordPayment']);
|
||||
});
|
||||
});
|
||||
|
||||
Route::prefix('reimbursements')->group(function () {
|
||||
Route::get('under-processing', [ReimbursementController::class, 'underProcessing']);
|
||||
@@ -1087,7 +1102,7 @@ Route::prefix('v1')->group(function () {
|
||||
Route::post('generate', [InvoiceController::class, 'generate']);
|
||||
Route::get('parent/{parentId}', [InvoiceController::class, 'byParent']);
|
||||
Route::get('parent-payment', [InvoiceController::class, 'parentPayment']);
|
||||
Route::post('{invoiceId}/status', [InvoiceController::class, 'updateStatus']);
|
||||
Route::post('{invoiceId}/status', [InvoiceController::class, 'updateStatus'])->middleware('admin.access');
|
||||
Route::get('unpaid', [InvoiceController::class, 'unpaid']);
|
||||
Route::get('{invoiceId}/preview', [InvoiceController::class, 'preview']);
|
||||
Route::get('{invoiceId}/pdf', [InvoiceController::class, 'pdf']);
|
||||
@@ -1113,16 +1128,18 @@ Route::prefix('v1')->group(function () {
|
||||
Route::get('parents/{parentId}/students', [PaymentEventChargesController::class, 'enrolledStudents']);
|
||||
});
|
||||
|
||||
Route::prefix('payment-notifications')->group(function () {
|
||||
Route::middleware('admin.access')->prefix('payment-notifications')->group(function () {
|
||||
Route::get('/', [PaymentNotificationController::class, 'index']);
|
||||
Route::post('send', [PaymentNotificationController::class, 'send']);
|
||||
});
|
||||
|
||||
Route::prefix('payment-transactions')->group(function () {
|
||||
Route::post('/', [PaymentTransactionController::class, 'store']);
|
||||
Route::get('payment/{paymentId}', [PaymentTransactionController::class, 'byPayment']);
|
||||
Route::middleware('admin.access')->group(function () {
|
||||
Route::post('/', [PaymentTransactionController::class, 'store']);
|
||||
Route::post('{transactionId}/status', [PaymentTransactionController::class, 'updateStatus']);
|
||||
});
|
||||
});
|
||||
|
||||
Route::prefix('paypal-transactions')->group(function () {
|
||||
Route::get('/', [PaypalTransactionsController::class, 'index']);
|
||||
@@ -1151,7 +1168,7 @@ Route::prefix('v1')->group(function () {
|
||||
Route::prefix('reports/slips')->group(function () {
|
||||
Route::post('print', [SlipPrinterController::class, 'print']);
|
||||
Route::post('preview', [SlipPrinterController::class, 'preview']);
|
||||
Route::get('logs', [SlipPrinterController::class, 'logs']);
|
||||
Route::get('logs', [SlipPrinterController::class, 'logs'])->middleware('admin.access');
|
||||
Route::post('reprint', [SlipPrinterController::class, 'reprint']);
|
||||
});
|
||||
Route::prefix('reports/stickers')->group(function () {
|
||||
@@ -1185,7 +1202,7 @@ Route::prefix('v1')->group(function () {
|
||||
Route::post('admin/review', [ExamDraftApiController::class, 'adminReview']);
|
||||
});
|
||||
|
||||
Route::prefix('configuration')->group(function () {
|
||||
Route::middleware('admin.access')->prefix('configuration')->group(function () {
|
||||
Route::get('/', [ConfigurationAdminController::class, 'index']);
|
||||
Route::get('key/{config_key}', [ConfigurationAdminController::class, 'getByKey']);
|
||||
Route::post('/', [ConfigurationAdminController::class, 'store']);
|
||||
@@ -1205,7 +1222,7 @@ Route::prefix('v1')->group(function () {
|
||||
|
||||
Route::prefix('incidents')->group(function () {
|
||||
Route::get('current', [IncidentApiController::class, 'current']);
|
||||
Route::get('history', [IncidentApiController::class, 'history']);
|
||||
Route::get('history', [IncidentApiController::class, 'history'])->middleware('admin.access');
|
||||
Route::get('processed', [IncidentApiController::class, 'processed']);
|
||||
Route::get('analysis', [IncidentApiController::class, 'analysis']);
|
||||
Route::get('grades/{gradeId}/students', [IncidentApiController::class, 'studentsByGrade']);
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\Configuration;
|
||||
use App\Models\Role;
|
||||
use App\Models\SchoolYear;
|
||||
use App\Models\User;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
trait CreatesApiTestUsers
|
||||
{
|
||||
@@ -68,6 +69,10 @@ trait CreatesApiTestUsers
|
||||
'status' => 'Active',
|
||||
'is_verified' => 1,
|
||||
'is_suspended' => 0,
|
||||
'address_street' => '1 Test Street',
|
||||
'city' => 'Brooklyn',
|
||||
'state' => 'NY',
|
||||
'zip' => '11201',
|
||||
'email' => $roleName.'-api-test-'.str_replace('.', '', uniqid('', true)).'@example.test',
|
||||
], $overrides));
|
||||
|
||||
@@ -81,6 +86,7 @@ trait CreatesApiTestUsers
|
||||
{
|
||||
$user = $this->createApiUserWithRole('administrator');
|
||||
$this->actingAs($user, 'api');
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -93,10 +93,10 @@ class ApiUseCaseCoverageMatrixTest extends TestCase
|
||||
'students_parents_and_families' => '#^api/v1/(?:students|parents|families|family-admin|emergency-contacts)(?:/|$)#',
|
||||
'teachers_staff_and_class_operations' => '#^api/v1/(?:teacher|teachers|staff|assignments|class-prep|class-progress)(?:/|$)#',
|
||||
'attendance_and_safety' => '#^api/v1/(?:attendance|attendance-tracking|attendance-comment-templates|attendance-templates|incidents)(?:/|$)#',
|
||||
'grading_scores_and_reporting' => '#^api/v1/(?:scores|grading|reports|badges|certificates|print-requests)(?:/|$)#',
|
||||
'grading_scores_and_reporting' => '#^api/v1/(?:scores|grading|reports|badges|certificates|print-requests|subjects/curriculum|exams/drafts)(?:/|$)#',
|
||||
'finance_billing_and_inventory' => '#^api/v1/(?:finance|discounts|expenses|extra-charges|inventory|files)(?:/|$)#',
|
||||
'communications_and_messaging' => '#^api/v1/(?:messages|broadcast-email|email|email-extractor|communications|support|whatsapp)(?:/|$)#',
|
||||
'competition_and_public_results' => '#^api/v1/(?:competition-scores)(?:/|$)#',
|
||||
'competition_and_public_results' => '#^api/v1/(?:competition-scores|competition-winners)(?:/|$)#',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -7,16 +7,17 @@ use App\Models\Student;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\Concerns\CreatesApiTestUsers;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EmergencyContactControllerTest extends TestCase
|
||||
{
|
||||
use CreatesApiTestUsers;
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_grouped_contacts(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$this->actingAsApiAdministrator();
|
||||
$parent = User::factory()->create([
|
||||
'firstname' => 'Omar',
|
||||
'lastname' => 'Parent',
|
||||
@@ -54,7 +55,6 @@ class EmergencyContactControllerTest extends TestCase
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($admin, [], 'api');
|
||||
$response = $this->getJson('/api/v1/administrator/emergency-contacts');
|
||||
|
||||
$response->assertOk();
|
||||
@@ -66,7 +66,7 @@ class EmergencyContactControllerTest extends TestCase
|
||||
|
||||
public function test_update_changes_emergency_contact(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$this->actingAsApiAdministrator();
|
||||
$parent = User::factory()->create();
|
||||
|
||||
$contact = EmergencyContact::query()->create([
|
||||
@@ -79,7 +79,6 @@ class EmergencyContactControllerTest extends TestCase
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($admin, [], 'api');
|
||||
$response = $this->patch('/api/v1/administrator/emergency-contacts/'.$contact->id, [
|
||||
'parent_id' => $parent->id,
|
||||
'name' => 'New Name',
|
||||
@@ -102,7 +101,7 @@ class EmergencyContactControllerTest extends TestCase
|
||||
|
||||
public function test_destroy_deletes_emergency_contact(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$this->actingAsApiAdministrator();
|
||||
$parent = User::factory()->create();
|
||||
|
||||
$contact = EmergencyContact::query()->create([
|
||||
@@ -115,7 +114,6 @@ class EmergencyContactControllerTest extends TestCase
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($admin, [], 'api');
|
||||
$response = $this->delete('/api/v1/administrator/emergency-contacts/'.$contact->id, [
|
||||
'parent_id' => $parent->id,
|
||||
], [
|
||||
@@ -131,7 +129,7 @@ class EmergencyContactControllerTest extends TestCase
|
||||
|
||||
public function test_show_returns_single_contact_for_parent(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$this->actingAsApiAdministrator();
|
||||
$parent = User::factory()->create();
|
||||
|
||||
$contact = EmergencyContact::query()->create([
|
||||
@@ -144,7 +142,6 @@ class EmergencyContactControllerTest extends TestCase
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($admin, [], 'api');
|
||||
$response = $this->get('/api/v1/administrator/emergency-contacts/'.$contact->id.'?parent_id='.$parent->id, [
|
||||
'Accept' => 'application/json',
|
||||
]);
|
||||
@@ -156,7 +153,7 @@ class EmergencyContactControllerTest extends TestCase
|
||||
|
||||
public function test_update_requires_matching_parent_id(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$this->actingAsApiAdministrator();
|
||||
$parent = User::factory()->create();
|
||||
$otherParent = User::factory()->create();
|
||||
|
||||
@@ -170,7 +167,6 @@ class EmergencyContactControllerTest extends TestCase
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($admin, [], 'api');
|
||||
$response = $this->patch('/api/v1/administrator/emergency-contacts/'.$contact->id, [
|
||||
'parent_id' => $otherParent->id,
|
||||
'name' => 'New Name',
|
||||
@@ -190,7 +186,7 @@ class EmergencyContactControllerTest extends TestCase
|
||||
|
||||
public function test_destroy_requires_matching_parent_id(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$this->actingAsApiAdministrator();
|
||||
$parent = User::factory()->create();
|
||||
$otherParent = User::factory()->create();
|
||||
|
||||
@@ -204,7 +200,6 @@ class EmergencyContactControllerTest extends TestCase
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($admin, [], 'api');
|
||||
$response = $this->delete('/api/v1/administrator/emergency-contacts/'.$contact->id, [
|
||||
'parent_id' => $otherParent->id,
|
||||
], [
|
||||
|
||||
@@ -36,6 +36,7 @@ class LateSlipLogsControllerTest extends TestCase
|
||||
'grade' => '3',
|
||||
'reason' => 'Traffic',
|
||||
'admin_name' => 'Admin User',
|
||||
'printed_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/attendance/late-slip-logs?school_year=2025-2026');
|
||||
@@ -59,6 +60,7 @@ class LateSlipLogsControllerTest extends TestCase
|
||||
'grade' => '2',
|
||||
'reason' => 'Bus delay',
|
||||
'admin_name' => 'Admin User',
|
||||
'printed_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/attendance/late-slip-logs/1');
|
||||
@@ -81,6 +83,7 @@ class LateSlipLogsControllerTest extends TestCase
|
||||
'grade' => '1',
|
||||
'reason' => 'Weather',
|
||||
'admin_name' => 'Admin User',
|
||||
'printed_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->deleteJson('/api/v1/attendance/late-slip-logs/1');
|
||||
|
||||
@@ -30,6 +30,7 @@ class IpBanControllerTest extends TestCase
|
||||
DB::table('ip_attempts')->insert([
|
||||
'ip_address' => '10.0.0.1',
|
||||
'attempts' => 5,
|
||||
'last_attempt_at' => now('UTC'),
|
||||
'blocked_until' => now('UTC')->addHours(2),
|
||||
]);
|
||||
|
||||
@@ -48,6 +49,7 @@ class IpBanControllerTest extends TestCase
|
||||
DB::table('ip_attempts')->insert([
|
||||
'ip_address' => '10.0.0.2',
|
||||
'attempts' => 3,
|
||||
'last_attempt_at' => now('UTC'),
|
||||
'blocked_until' => now('UTC')->addHours(1),
|
||||
]);
|
||||
|
||||
@@ -65,6 +67,7 @@ class IpBanControllerTest extends TestCase
|
||||
DB::table('ip_attempts')->insert([
|
||||
'ip_address' => '10.0.0.3',
|
||||
'attempts' => 1,
|
||||
'last_attempt_at' => now('UTC'),
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/v1/ip-bans/ban', [
|
||||
@@ -87,6 +90,7 @@ class IpBanControllerTest extends TestCase
|
||||
DB::table('ip_attempts')->insert([
|
||||
'ip_address' => '10.0.0.4',
|
||||
'attempts' => 5,
|
||||
'last_attempt_at' => now('UTC'),
|
||||
'blocked_until' => now('UTC')->addHours(2),
|
||||
]);
|
||||
|
||||
@@ -111,11 +115,13 @@ class IpBanControllerTest extends TestCase
|
||||
[
|
||||
'ip_address' => '10.0.0.5',
|
||||
'attempts' => 5,
|
||||
'last_attempt_at' => now('UTC'),
|
||||
'blocked_until' => now('UTC')->addHours(2),
|
||||
],
|
||||
[
|
||||
'ip_address' => '10.0.0.6',
|
||||
'attempts' => 2,
|
||||
'last_attempt_at' => now('UTC'),
|
||||
'blocked_until' => now('UTC')->addHours(2),
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -50,10 +50,14 @@ class ClassPreparationControllerTest extends TestCase
|
||||
|
||||
private function seedStickerData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
DB::table('configuration')->updateOrInsert(
|
||||
['config_key' => 'school_year'],
|
||||
['config_value' => '2025-2026'],
|
||||
);
|
||||
DB::table('configuration')->updateOrInsert(
|
||||
['config_key' => 'semester'],
|
||||
['config_value' => 'Fall'],
|
||||
);
|
||||
|
||||
DB::table('classes')->insert([
|
||||
[
|
||||
|
||||
@@ -61,6 +61,8 @@ class ClassProgressControllerTest extends TestCase
|
||||
'covered_quran' => 'Lesson B',
|
||||
'homework_islamic' => 'HW A',
|
||||
'homework_quran' => 'HW B',
|
||||
'unit_islamic' => ['Unit 1'],
|
||||
'chapter_islamic' => ['Chapter A'],
|
||||
'flags' => ['needs_support'],
|
||||
];
|
||||
|
||||
|
||||
@@ -74,10 +74,14 @@ class CompetitionScoresControllerTest extends TestCase
|
||||
|
||||
private function seedCompetitionData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
DB::table('configuration')->updateOrInsert(
|
||||
['config_key' => 'school_year'],
|
||||
['config_value' => '2025-2026']
|
||||
);
|
||||
DB::table('configuration')->updateOrInsert(
|
||||
['config_key' => 'semester'],
|
||||
['config_value' => 'Fall']
|
||||
);
|
||||
|
||||
DB::table('classes')->insert([
|
||||
'id' => 1,
|
||||
|
||||
@@ -88,10 +88,14 @@ class CompetitionWinnersControllerTest extends TestCase
|
||||
|
||||
private function seedCompetitionData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
DB::table('configuration')->updateOrInsert(
|
||||
['config_key' => 'school_year'],
|
||||
['config_value' => '2025-2026']
|
||||
);
|
||||
DB::table('configuration')->updateOrInsert(
|
||||
['config_key' => 'semester'],
|
||||
['config_value' => 'Fall']
|
||||
);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
|
||||
@@ -101,10 +101,14 @@ class DiscountControllerTest extends TestCase
|
||||
|
||||
private function seedDiscountData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
DB::table('configuration')->updateOrInsert(
|
||||
['config_key' => 'school_year'],
|
||||
['config_value' => '2025-2026']
|
||||
);
|
||||
DB::table('configuration')->updateOrInsert(
|
||||
['config_key' => 'semester'],
|
||||
['config_value' => 'Fall']
|
||||
);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 10,
|
||||
|
||||
@@ -129,6 +129,7 @@ class ExamDraftControllerTest extends TestCase
|
||||
'class_section_id' => $classSectionId,
|
||||
'position' => 'main',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
@@ -169,10 +169,14 @@ class ExpenseControllerTest extends TestCase
|
||||
|
||||
private function seedExpenseData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
DB::table('configuration')->updateOrInsert(
|
||||
['config_key' => 'school_year'],
|
||||
['config_value' => '2025-2026']
|
||||
);
|
||||
DB::table('configuration')->updateOrInsert(
|
||||
['config_key' => 'semester'],
|
||||
['config_value' => 'Fall']
|
||||
);
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => 2,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user