update api and add more features
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Administrator;
|
||||
|
||||
use App\Http\Controllers\Api\Core\BaseApiController;
|
||||
use App\Http\Requests\Promotions\AdminListPromotionsRequest;
|
||||
use App\Http\Requests\Promotions\EvaluateEligibilityRequest;
|
||||
use App\Http\Requests\Promotions\ParentEnrollmentStepRequest;
|
||||
use App\Http\Requests\Promotions\SendReminderRequest;
|
||||
use App\Http\Requests\Promotions\SetEnrollmentDeadlineRequest;
|
||||
use App\Http\Requests\Promotions\UpdatePromotionStatusRequest;
|
||||
use App\Http\Requests\Promotions\UpsertLevelProgressionRequest;
|
||||
use App\Http\Resources\Promotions\LevelProgressionResource;
|
||||
use App\Http\Resources\Promotions\PromotionAuditLogResource;
|
||||
use App\Http\Resources\Promotions\PromotionReminderResource;
|
||||
use App\Http\Resources\Promotions\StudentPromotionResource;
|
||||
use App\Models\PromotionAuditLog;
|
||||
use App\Models\PromotionReminderLog;
|
||||
use App\Models\StudentPromotionRecord;
|
||||
use App\Services\Promotions\LevelProgressionService;
|
||||
use App\Services\Promotions\PromotionAuditService;
|
||||
use App\Services\Promotions\PromotionEligibilityService;
|
||||
use App\Services\Promotions\PromotionEnrollmentService;
|
||||
use App\Services\Promotions\PromotionQueryService;
|
||||
use App\Services\Promotions\PromotionReminderService;
|
||||
use App\Services\Promotions\PromotionStatusService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Admin / registrar API for managing the promotion + parent enrollment
|
||||
* lifecycle (plan sections 13, 16, 17).
|
||||
*/
|
||||
class AdministratorPromotionController extends BaseApiController
|
||||
{
|
||||
public function __construct(
|
||||
private PromotionQueryService $query,
|
||||
private PromotionEligibilityService $eligibility,
|
||||
private PromotionStatusService $statusService,
|
||||
private PromotionEnrollmentService $enrollmentService,
|
||||
private PromotionReminderService $reminders,
|
||||
private PromotionAuditService $audit,
|
||||
private LevelProgressionService $progression
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index(AdminListPromotionsRequest $request): JsonResponse
|
||||
{
|
||||
$result = $this->query->list($request->filters());
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => StudentPromotionResource::collection($result['data']),
|
||||
'pagination' => [
|
||||
'page' => $result['page'],
|
||||
'per_page' => $result['per_page'],
|
||||
'total' => $result['total'],
|
||||
'total_pages' => $result['total_pages'],
|
||||
],
|
||||
'counts_by_status' => $result['counts_by_status'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(int $promotionId): JsonResponse
|
||||
{
|
||||
$record = $this->findOrFail($promotionId);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => new StudentPromotionResource($this->query->detail($record)),
|
||||
]);
|
||||
}
|
||||
|
||||
public function summary(AdminListPromotionsRequest $request): JsonResponse
|
||||
{
|
||||
$counts = $this->query->countsByStatus($request->filters());
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'counts_by_status' => $counts,
|
||||
'total' => array_sum($counts),
|
||||
]);
|
||||
}
|
||||
|
||||
public function evaluate(EvaluateEligibilityRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$userId = $this->getCurrentUserId();
|
||||
$scope = $payload['scope'] ?? 'school_year';
|
||||
|
||||
try {
|
||||
$result = match ($scope) {
|
||||
'student' => [
|
||||
'scope' => 'student',
|
||||
'record' => optional(
|
||||
$this->eligibility->evaluateStudent(
|
||||
(int) $payload['student_id'],
|
||||
$payload['current_school_year'] ?? null,
|
||||
$userId
|
||||
)
|
||||
)?->toArray(),
|
||||
],
|
||||
'class_section' => array_merge(
|
||||
['scope' => 'class_section'],
|
||||
$this->eligibility->evaluateClassSection(
|
||||
(int) $payload['class_section_id'],
|
||||
$payload['current_school_year'] ?? null,
|
||||
$userId
|
||||
)
|
||||
),
|
||||
default => array_merge(
|
||||
['scope' => 'school_year'],
|
||||
$this->eligibility->evaluateSchoolYear(
|
||||
$payload['current_school_year'] ?? null,
|
||||
$userId
|
||||
)
|
||||
),
|
||||
};
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Promotion eligibility evaluation failed', ['exception' => $e]);
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => 'Failed to evaluate promotion eligibility.',
|
||||
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'result' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateStatus(UpdatePromotionStatusRequest $request, int $promotionId): JsonResponse
|
||||
{
|
||||
$record = $this->findOrFail($promotionId);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
$userId = $this->getCurrentUserId();
|
||||
|
||||
try {
|
||||
$updated = !empty($payload['force'])
|
||||
? $this->statusService->forceStatus($record, $payload['status'], $userId, $payload['notes'] ?? null)
|
||||
: $this->statusService->transition($record, $payload['status'], $userId, $payload['notes'] ?? null);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => new StudentPromotionResource($this->query->detail($updated->refresh())),
|
||||
]);
|
||||
}
|
||||
|
||||
public function setDeadline(SetEnrollmentDeadlineRequest $request, ?int $promotionId = null): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$userId = $this->getCurrentUserId();
|
||||
$newDeadline = $payload['enrollment_deadline'];
|
||||
$applyTo = $payload['apply_to'] ?? ($promotionId ? 'single' : 'school_year');
|
||||
|
||||
$records = collect();
|
||||
if ($promotionId !== null || $applyTo === 'single') {
|
||||
$id = $promotionId ?? null;
|
||||
if ($id === null) {
|
||||
return response()->json(['ok' => false, 'message' => 'promotion_id is required for single deadline updates.'], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
$record = $this->findOrFail($id);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
$records = collect([$record]);
|
||||
} elseif ($applyTo === 'filter' && !empty($payload['promotion_ids'])) {
|
||||
$records = StudentPromotionRecord::query()
|
||||
->whereIn('promotion_id', $payload['promotion_ids'])
|
||||
->get();
|
||||
} else {
|
||||
$year = $payload['next_school_year'] ?? null;
|
||||
if ($year === null || $year === '') {
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => 'next_school_year is required when applying a bulk deadline.',
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
$records = StudentPromotionRecord::query()
|
||||
->forNextYear($year)
|
||||
->whereIn('promotion_status', StudentPromotionRecord::openStatuses())
|
||||
->get();
|
||||
}
|
||||
|
||||
$updatedIds = [];
|
||||
foreach ($records as $record) {
|
||||
$oldDeadline = $record->enrollment_deadline?->toDateString();
|
||||
DB::transaction(function () use ($record, $newDeadline, $oldDeadline, $userId, &$updatedIds) {
|
||||
$record->enrollment_deadline = $newDeadline;
|
||||
$record->updated_by = $userId;
|
||||
$record->save();
|
||||
$this->audit->logDeadlineSet($record, $oldDeadline, $newDeadline, $userId);
|
||||
$updatedIds[] = (int) $record->getKey();
|
||||
});
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'updated' => count($updatedIds),
|
||||
'promotion_ids' => $updatedIds,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendReminder(SendReminderRequest $request, int $promotionId): JsonResponse
|
||||
{
|
||||
$record = $this->findOrFail($promotionId);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
$userId = $this->getCurrentUserId();
|
||||
|
||||
try {
|
||||
$log = $this->reminders->send(
|
||||
$record,
|
||||
$payload['reminder_type'] ?? PromotionReminderLog::TYPE_MANUAL,
|
||||
$userId,
|
||||
$payload['subject'] ?? null,
|
||||
$payload['message'] ?? null,
|
||||
$payload['channels'] ?? ['in_app', 'email']
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Promotion reminder send failed', [
|
||||
'promotion_id' => $promotionId,
|
||||
'exception' => $e,
|
||||
]);
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => 'Failed to send reminder.',
|
||||
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'reminder' => new PromotionReminderResource($log),
|
||||
]);
|
||||
}
|
||||
|
||||
public function dispatchScheduledReminders(): JsonResponse
|
||||
{
|
||||
$userId = $this->getCurrentUserId();
|
||||
$result = $this->reminders->dispatchScheduledReminders(null, $userId);
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'result' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
public function reminders(int $promotionId): JsonResponse
|
||||
{
|
||||
$record = $this->findOrFail($promotionId);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
$rows = PromotionReminderLog::query()
|
||||
->forPromotion((int) $record->getKey())
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'reminders' => PromotionReminderResource::collection($rows),
|
||||
]);
|
||||
}
|
||||
|
||||
public function audit(int $promotionId): JsonResponse
|
||||
{
|
||||
$record = $this->findOrFail($promotionId);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
$rows = PromotionAuditLog::query()
|
||||
->forPromotion((int) $record->getKey())
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'entries' => PromotionAuditLogResource::collection($rows),
|
||||
]);
|
||||
}
|
||||
|
||||
public function expireDeadlines(): JsonResponse
|
||||
{
|
||||
$result = $this->enrollmentService->markExpiredDeadlines(null, $this->getCurrentUserId());
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'result' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
public function adminCompleteEnrollment(ParentEnrollmentStepRequest $request, int $promotionId): JsonResponse
|
||||
{
|
||||
$record = $this->findOrFail($promotionId);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
$userId = $this->getCurrentUserId();
|
||||
$parentId = (int) $record->parent_id ?: $userId ?: 0;
|
||||
|
||||
if ($parentId <= 0) {
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => 'Promotion record has no associated parent.',
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
try {
|
||||
$updated = $this->enrollmentService->updateSteps(
|
||||
$record,
|
||||
$parentId,
|
||||
$request->steps(),
|
||||
$userId
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => new StudentPromotionResource($this->query->detail($updated->refresh())),
|
||||
]);
|
||||
}
|
||||
|
||||
public function levelProgressions(): JsonResponse
|
||||
{
|
||||
$rows = $this->progression->list(false);
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'levels' => LevelProgressionResource::collection($rows),
|
||||
]);
|
||||
}
|
||||
|
||||
public function upsertLevelProgression(UpsertLevelProgressionRequest $request): JsonResponse
|
||||
{
|
||||
$row = $this->progression->upsertMapping($request->validated());
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'level' => new LevelProgressionResource($row),
|
||||
]);
|
||||
}
|
||||
|
||||
public function seedLevelProgressions(): JsonResponse
|
||||
{
|
||||
$created = $this->progression->seedDefaults();
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'created' => $created,
|
||||
]);
|
||||
}
|
||||
|
||||
private function findOrFail(int $promotionId): StudentPromotionRecord|JsonResponse
|
||||
{
|
||||
$record = StudentPromotionRecord::query()->find($promotionId);
|
||||
if (!$record) {
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => 'Promotion record not found.',
|
||||
], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
return $record;
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,7 @@ class AuthController extends BaseApiController
|
||||
$payload = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
// Match CodeIgniter apiLogin: raw email/password (no trim); empty check is identical to CI `!$email || !$password`.
|
||||
$email = (string) ($payload['email'] ?? '');
|
||||
$email = strtolower(trim((string) ($payload['email'] ?? '')));
|
||||
$password = (string) ($payload['password'] ?? '');
|
||||
|
||||
if ($email === '' || $password === '') {
|
||||
@@ -39,7 +38,7 @@ class AuthController extends BaseApiController
|
||||
], 429);
|
||||
}
|
||||
|
||||
$user = User::query()->where('email', $email)->first();
|
||||
$user = User::query()->whereRaw('LOWER(email) = ?', [$email])->first();
|
||||
if (!$user) {
|
||||
$security->logIpAttempt((string) $ip);
|
||||
|
||||
@@ -49,13 +48,6 @@ class AuthController extends BaseApiController
|
||||
], 401);
|
||||
}
|
||||
|
||||
if ($user->is_suspended) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'Account suspended. Please reset your password.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
if (! ci_password_verify($password, (string) $user->password)) {
|
||||
$security->handleFailedLogin($user, $email, (string) $ip);
|
||||
|
||||
@@ -65,6 +57,15 @@ class AuthController extends BaseApiController
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Suspension is checked AFTER credentials are verified so that the
|
||||
// response shape cannot be used to enumerate which emails exist.
|
||||
if ($user->is_suspended) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'Account suspended. Please reset your password.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$security->resetFailedAttempts($user);
|
||||
$security->logSuccessfulLogin($user->fresh(), $request);
|
||||
|
||||
|
||||
@@ -245,28 +245,31 @@ class BaseApiController extends Controller
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = app(User::class);
|
||||
$row = $user->find($userId);
|
||||
/** @var User|null $row */
|
||||
$row = User::query()->find($userId);
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? ''));
|
||||
$firstname = (string) ($row->firstname ?? '');
|
||||
$lastname = (string) ($row->lastname ?? '');
|
||||
$email = $row->email;
|
||||
|
||||
$name = trim($firstname . ' ' . $lastname);
|
||||
if ($name === '') {
|
||||
$name = $row['email'] ?? 'User #' . $userId;
|
||||
$name = $email ?? 'User #' . $userId;
|
||||
}
|
||||
|
||||
try {
|
||||
$roles = DB::table('user_roles ur')
|
||||
->select('LOWER(r.name) AS name')
|
||||
->join('roles r', 'r.id', '=', 'ur.role_id')
|
||||
$roles = DB::table('user_roles as ur')
|
||||
->join('roles as r', 'r.id', '=', 'ur.role_id')
|
||||
->where('ur.user_id', $userId)
|
||||
->whereNull('ur.deleted_at')
|
||||
->pluck('name')
|
||||
->map(fn($name) => strtolower((string) $name))
|
||||
->pluck('r.name')
|
||||
->map(fn ($name) => strtolower((string) $name))
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
->all();
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Unable to load user roles: ' . $e->getMessage());
|
||||
$roles = [];
|
||||
@@ -275,7 +278,7 @@ class BaseApiController extends Controller
|
||||
return (object) [
|
||||
'id' => (int) $userId,
|
||||
'name' => $name,
|
||||
'email' => $row['email'] ?? null,
|
||||
'email' => $email,
|
||||
'roles' => $roles,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Finance;
|
||||
|
||||
use App\Http\Controllers\Api\Core\BaseApiController;
|
||||
use App\Http\Requests\Finance\ApplyExtraChargeRequest;
|
||||
use App\Http\Requests\Finance\ChargeListRequest;
|
||||
use App\Http\Requests\Finance\MarkEventChargePaidRequest;
|
||||
use App\Http\Requests\Finance\StoreEventChargeRequest;
|
||||
use App\Http\Requests\Finance\StoreExtraChargeRequest;
|
||||
use App\Http\Resources\Fees\ChargeActionResource;
|
||||
use App\Http\Resources\Fees\ChargeListResource;
|
||||
use App\Services\Billing\ChargeService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ChargeController extends BaseApiController
|
||||
{
|
||||
public function __construct(private ChargeService $charges)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index(ChargeListRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$result = $this->charges->listForParent(
|
||||
(int) $payload['parent_id'],
|
||||
$payload['school_year'] ?? null
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'charges' => new ChargeListResource($result),
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeExtra(StoreExtraChargeRequest $request): JsonResponse
|
||||
{
|
||||
$guard = $this->authenticatedUserIdOrUnauthorized();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
$payload['created_by'] = $guard;
|
||||
|
||||
try {
|
||||
$result = $this->charges->createExtraCharge($payload);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Store extra charge failed', ['exception' => $e]);
|
||||
|
||||
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;
|
||||
|
||||
return response()->json([
|
||||
'ok' => (bool) ($result['ok'] ?? false),
|
||||
'charge' => new ChargeActionResource($result),
|
||||
], $status);
|
||||
}
|
||||
|
||||
public function storeEvent(StoreEventChargeRequest $request): JsonResponse
|
||||
{
|
||||
$guard = $this->authenticatedUserIdOrUnauthorized();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
$payload['created_by'] = $guard;
|
||||
|
||||
try {
|
||||
$result = $this->charges->createEventCharge($payload);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Store event charge failed', ['exception' => $e]);
|
||||
|
||||
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;
|
||||
|
||||
return response()->json([
|
||||
'ok' => (bool) ($result['ok'] ?? false),
|
||||
'charge' => new ChargeActionResource($result),
|
||||
], $status);
|
||||
}
|
||||
|
||||
public function cancelExtra(int $id): JsonResponse
|
||||
{
|
||||
$result = $this->charges->cancelExtraCharge($id);
|
||||
$status = ($result['ok'] ?? false) ? 200 : 422;
|
||||
|
||||
return response()->json([
|
||||
'ok' => (bool) ($result['ok'] ?? false),
|
||||
'charge' => new ChargeActionResource($result),
|
||||
], $status);
|
||||
}
|
||||
|
||||
public function applyExtra(ApplyExtraChargeRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$result = $this->charges->applyExtraCharge($id, (int) $payload['invoice_id']);
|
||||
$status = ($result['ok'] ?? false) ? 200 : 422;
|
||||
|
||||
return response()->json([
|
||||
'ok' => (bool) ($result['ok'] ?? false),
|
||||
'charge' => new ChargeActionResource($result),
|
||||
], $status);
|
||||
}
|
||||
|
||||
public function cancelEvent(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$issueCredit = $request->boolean('issue_credit', true);
|
||||
$result = $this->charges->cancelEventCharge($id, $issueCredit);
|
||||
$status = ($result['ok'] ?? false) ? 200 : 422;
|
||||
|
||||
return response()->json([
|
||||
'ok' => (bool) ($result['ok'] ?? false),
|
||||
'charge' => new ChargeActionResource($result),
|
||||
], $status);
|
||||
}
|
||||
|
||||
public function markEventPaid(MarkEventChargePaidRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$result = $this->charges->markEventChargePaid(
|
||||
$id,
|
||||
(bool) $payload['paid'],
|
||||
isset($payload['payment_id']) ? (int) $payload['payment_id'] : null
|
||||
);
|
||||
$status = ($result['ok'] ?? false) ? 200 : 422;
|
||||
|
||||
return response()->json([
|
||||
'ok' => (bool) ($result['ok'] ?? false),
|
||||
'charge' => new ChargeActionResource($result),
|
||||
], $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|JsonResponse
|
||||
*/
|
||||
private function authenticatedUserIdOrUnauthorized(): int|JsonResponse
|
||||
{
|
||||
$userId = (int) (auth()->id() ?? 0);
|
||||
if ($userId <= 0) {
|
||||
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
|
||||
}
|
||||
|
||||
return $userId;
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,18 @@
|
||||
namespace App\Http\Controllers\Api\Finance;
|
||||
|
||||
use App\Http\Controllers\Api\Core\BaseApiController;
|
||||
use App\Http\Requests\Finance\FeeFamilyBalanceRequest;
|
||||
use App\Http\Requests\Finance\FeeRefundRequest;
|
||||
use App\Http\Requests\Finance\FeeTuitionBreakdownRequest;
|
||||
use App\Http\Requests\Finance\FeeTuitionTotalRequest;
|
||||
use App\Http\Resources\Fees\FeeFamilyBalanceResource;
|
||||
use App\Http\Resources\Fees\FeeRefundResource;
|
||||
use App\Http\Resources\Fees\FeeTuitionBreakdownResource;
|
||||
use App\Http\Resources\Fees\FeeTuitionTotalResource;
|
||||
use App\Services\Billing\BalanceCalculationService;
|
||||
use App\Services\Fees\FeeRefundCalculatorService;
|
||||
use App\Services\Fees\FeeStudentFeeService;
|
||||
use App\Services\Fees\TuitionCalculationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -16,7 +22,9 @@ class FeeCalculationController extends BaseApiController
|
||||
{
|
||||
public function __construct(
|
||||
private FeeRefundCalculatorService $refunds,
|
||||
private FeeStudentFeeService $tuition
|
||||
private FeeStudentFeeService $tuition,
|
||||
private TuitionCalculationService $tuitionCalculator,
|
||||
private BalanceCalculationService $balance
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -69,4 +77,55 @@ class FeeCalculationController extends BaseApiController
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function tuitionBreakdown(FeeTuitionBreakdownRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
|
||||
try {
|
||||
$result = $this->tuitionCalculator->calculate($payload['students']);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Tuition breakdown calculation failed', [
|
||||
'exception' => $e,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => 'Unable to calculate tuition breakdown.',
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'tuition' => new FeeTuitionBreakdownResource($result),
|
||||
]);
|
||||
}
|
||||
|
||||
public function familyBalance(FeeFamilyBalanceRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
|
||||
try {
|
||||
$result = $this->balance->calculateFamilyAccount(
|
||||
$payload['students'],
|
||||
(int) $payload['parent_id'],
|
||||
$payload['school_year'] ?? null
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Family balance calculation failed', [
|
||||
'parent_id' => $payload['parent_id'] ?? null,
|
||||
'exception' => $e,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => 'Unable to calculate family balance.',
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'account' => new FeeFamilyBalanceResource($result),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,14 +37,19 @@ class AuthorizedUserInviteController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
$redirectUrl = (string) $result['redirect_url'];
|
||||
if (! $this->isTrustedRedirectUrl($redirectUrl)) {
|
||||
return response()->json(['message' => 'Invalid redirect URL.'], 400);
|
||||
}
|
||||
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'message' => 'Confirmed.',
|
||||
'redirect_url' => $result['redirect_url'],
|
||||
'redirect_url' => $redirectUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->away($result['redirect_url']);
|
||||
return redirect()->away($redirectUrl);
|
||||
}
|
||||
|
||||
public function setPasswordForm(Request $request, int $authorizedUserId): JsonResponse|Response
|
||||
@@ -79,7 +84,8 @@ class AuthorizedUserInviteController extends Controller
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'password' => ['required', 'string', 'min:8', 'regex:/[A-Z]/', 'regex:/[a-z]/', 'regex:/[0-9]/', 'regex:/[\W_]/'],
|
||||
'password_confirm' => ['required', 'same:password'],
|
||||
'password_confirmation' => ['required_without:password_confirm', 'same:password'],
|
||||
'password_confirm' => ['required_without:password_confirmation', 'same:password'],
|
||||
'user_id' => ['required', 'integer'],
|
||||
'token' => ['required', 'string'],
|
||||
], [
|
||||
@@ -108,4 +114,23 @@ class AuthorizedUserInviteController extends Controller
|
||||
|
||||
return response()->json(['message' => 'Password has been successfully set.']);
|
||||
}
|
||||
|
||||
private function isTrustedRedirectUrl(string $url): bool
|
||||
{
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
if (! is_string($host) || $host === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$trusted = [
|
||||
parse_url((string) config('app.url'), PHP_URL_HOST),
|
||||
parse_url((string) config('app.frontend_url'), PHP_URL_HOST),
|
||||
parse_url((string) config('services.frontend.url'), PHP_URL_HOST),
|
||||
];
|
||||
|
||||
return in_array(strtolower($host), array_filter(array_map(
|
||||
static fn ($value) => is_string($value) ? strtolower($value) : null,
|
||||
$trusted
|
||||
)), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,14 +58,17 @@ class AuthorizedUsersController extends BaseApiController
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$email = strtolower($request->validated('email'));
|
||||
$email = strtolower(trim((string) $request->validated('email')));
|
||||
$result = $this->svc->inviteByEmail($guard, $email);
|
||||
|
||||
$message = 'Authorized user added. A confirmation email has been sent.';
|
||||
if (! $result['created']) {
|
||||
return $this->success(['message' => $message], $message, Response::HTTP_CREATED);
|
||||
$message = 'If the email belongs to a registered user, a confirmation email has been sent.';
|
||||
|
||||
return $this->success(['message' => $message], $message, Response::HTTP_OK);
|
||||
}
|
||||
|
||||
$message = 'Authorized user added. A confirmation email has been sent.';
|
||||
|
||||
return $this->success(
|
||||
['message' => $message, 'id' => $result['record']->id],
|
||||
$message,
|
||||
@@ -89,11 +92,18 @@ class AuthorizedUsersController extends BaseApiController
|
||||
$row = $resolution;
|
||||
|
||||
$email = $request->validated('email') ?? null;
|
||||
if (is_string($email) && $email !== '') {
|
||||
$row->email = strtolower($email);
|
||||
}
|
||||
if (is_string($email) && trim($email) !== '') {
|
||||
try {
|
||||
$this->svc->changeEmailAndReinvite($row, $email);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->error($e->getMessage(), Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
$row->save();
|
||||
return $this->success(
|
||||
['message' => 'Authorized user email updated. A new confirmation email has been sent.'],
|
||||
'Authorized user email updated. A new confirmation email has been sent.'
|
||||
);
|
||||
}
|
||||
|
||||
return $this->success(['message' => 'Authorized user information updated.'], 'Authorized user information updated.');
|
||||
}
|
||||
@@ -127,15 +137,15 @@ class AuthorizedUsersController extends BaseApiController
|
||||
|
||||
private function authorizedUserForParent(int $id, int $parentId): AuthorizedUser|JsonResponse
|
||||
{
|
||||
$row = AuthorizedUser::query()->find($id);
|
||||
$row = AuthorizedUser::query()
|
||||
->where('id', $id)
|
||||
->where('user_id', $parentId)
|
||||
->first();
|
||||
|
||||
if (! $row) {
|
||||
return $this->error('Authorized user not found.', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ((int) $row->user_id !== $parentId) {
|
||||
return $this->error('You do not have access to this resource.', Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,10 +148,14 @@ class ParentController extends BaseApiController
|
||||
$payload['emergency_contacts'] ?? []
|
||||
);
|
||||
|
||||
$createdCount = count($result['created_student_ids']);
|
||||
$skipped = $result['skipped'] ?? [];
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'created_student_ids' => $result['created_student_ids'],
|
||||
], 201);
|
||||
'skipped' => $skipped,
|
||||
], $createdCount > 0 ? 201 : 200);
|
||||
}
|
||||
|
||||
public function updateStudent(ParentStudentUpdateRequest $request, int $studentId): JsonResponse
|
||||
@@ -283,10 +287,21 @@ class ParentController extends BaseApiController
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|JsonResponse Authenticated parent user id, or 401 JSON for this controller's legacy `{ ok }` shape.
|
||||
* @return int|JsonResponse Effective primary-parent user id, or 401 JSON for this controller's legacy `{ ok }` shape.
|
||||
*
|
||||
* `parent.access` middleware resolves second parents (user_type=secondary)
|
||||
* and authorized users (user_type=tertiary) to their primary parent and
|
||||
* stashes the result as `primary_parent_id` on the request. If we are
|
||||
* called outside that middleware we fall back to the authenticated id.
|
||||
*/
|
||||
private function parentIdOrUnauthorized(): int|JsonResponse
|
||||
{
|
||||
$request = request();
|
||||
$primary = (int) ($request?->attributes?->get('primary_parent_id') ?? 0);
|
||||
if ($primary > 0) {
|
||||
return $primary;
|
||||
}
|
||||
|
||||
$parentId = (int) (auth()->id() ?? 0);
|
||||
if ($parentId <= 0) {
|
||||
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Parents;
|
||||
|
||||
use App\Http\Controllers\Api\Core\BaseApiController;
|
||||
use App\Http\Requests\Promotions\ParentEnrollmentStepRequest;
|
||||
use App\Http\Requests\Promotions\ParentPromotionOverviewRequest;
|
||||
use App\Http\Resources\Promotions\StudentPromotionResource;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentPromotionRecord;
|
||||
use App\Services\Promotions\PromotionEnrollmentService;
|
||||
use App\Services\Promotions\PromotionQueryService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Parent portal endpoints for the promotion + enrollment workflow
|
||||
* (plan section 12). The parent sees the promoted level and can step
|
||||
* through the enrollment checklist defined in plan section 8.
|
||||
*/
|
||||
class ParentPromotionController extends BaseApiController
|
||||
{
|
||||
public function __construct(
|
||||
private PromotionEnrollmentService $service,
|
||||
private PromotionQueryService $query
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function overview(ParentPromotionOverviewRequest $request): JsonResponse
|
||||
{
|
||||
$guard = $this->parentIdOrUnauthorized();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$data = $this->service->overviewForParent(
|
||||
$guard,
|
||||
$request->validated()['next_school_year'] ?? null
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'records' => $data['records'],
|
||||
'next_school_year' => $data['next_school_year'],
|
||||
'message' => $data['message'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(int $promotionId): JsonResponse
|
||||
{
|
||||
$guard = $this->parentIdOrUnauthorized();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$record = $this->loadOwnedRecord($promotionId, $guard);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => new StudentPromotionResource($this->query->detail($record)),
|
||||
]);
|
||||
}
|
||||
|
||||
public function start(int $promotionId): JsonResponse
|
||||
{
|
||||
$guard = $this->parentIdOrUnauthorized();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$record = $this->loadOwnedRecord($promotionId, $guard);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
try {
|
||||
$updated = $this->service->startEnrollment($record, $guard, $guard);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => new StudentPromotionResource($this->query->detail($updated->refresh())),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateSteps(ParentEnrollmentStepRequest $request, int $promotionId): JsonResponse
|
||||
{
|
||||
$guard = $this->parentIdOrUnauthorized();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$record = $this->loadOwnedRecord($promotionId, $guard);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
try {
|
||||
$updated = $this->service->updateSteps($record, $guard, $request->steps(), $guard);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => new StudentPromotionResource($this->query->detail($updated->refresh())),
|
||||
]);
|
||||
}
|
||||
|
||||
public function submit(int $promotionId): JsonResponse
|
||||
{
|
||||
$guard = $this->parentIdOrUnauthorized();
|
||||
if ($guard instanceof JsonResponse) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
$record = $this->loadOwnedRecord($promotionId, $guard);
|
||||
if ($record instanceof JsonResponse) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
try {
|
||||
$updated = $this->service->submitEnrollment($record, $guard, $guard);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => new StudentPromotionResource($this->query->detail($updated->refresh())),
|
||||
]);
|
||||
}
|
||||
|
||||
private function loadOwnedRecord(int $promotionId, int $parentId): StudentPromotionRecord|JsonResponse
|
||||
{
|
||||
$record = StudentPromotionRecord::query()->find($promotionId);
|
||||
if (!$record) {
|
||||
return response()->json(['ok' => false, 'message' => 'Promotion record not found.'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$owns = (int) $record->parent_id === $parentId;
|
||||
if (!$owns) {
|
||||
$studentParentId = (int) (Student::query()
|
||||
->where('id', $record->student_id)
|
||||
->value('parent_id') ?? 0);
|
||||
$owns = $studentParentId === $parentId;
|
||||
}
|
||||
if (!$owns) {
|
||||
return response()->json(['ok' => false, 'message' => 'You are not authorised to view this promotion record.'], Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
return $record;
|
||||
}
|
||||
|
||||
private function parentIdOrUnauthorized(): int|JsonResponse
|
||||
{
|
||||
$request = request();
|
||||
$primary = (int) ($request?->attributes?->get('primary_parent_id') ?? 0);
|
||||
if ($primary > 0) {
|
||||
return $primary;
|
||||
}
|
||||
$parentId = (int) (auth()->id() ?? 0);
|
||||
if ($parentId <= 0) {
|
||||
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
return $parentId;
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*
|
||||
* These middleware run during every request to your application.
|
||||
*/
|
||||
protected $middleware = [
|
||||
// Handles CORS
|
||||
\Illuminate\Http\Middleware\HandleCors::class,
|
||||
|
||||
// Trust proxies
|
||||
\Illuminate\Http\Middleware\TrustProxies::class,
|
||||
|
||||
// Prevent request size attacks
|
||||
\Illuminate\Http\Middleware\ValidatePostSize::class,
|
||||
|
||||
// Trim whitespace
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
|
||||
// Convert empty strings to null
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
// Throttling (optional)
|
||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
||||
|
||||
// JWT Authentication
|
||||
\PHPOpenSourceSaver\JWTAuth\Http\Middleware\Authenticate::class,
|
||||
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware.
|
||||
*
|
||||
* These may be assigned to routes individually.
|
||||
*/
|
||||
protected $routeMiddleware = [
|
||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||
|
||||
// Laravel default middleware
|
||||
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
|
||||
// JWT middleware
|
||||
'jwt.auth' => \PHPOpenSourceSaver\JWTAuth\Http\Middleware\Authenticate::class,
|
||||
'jwt.refresh' => \PHPOpenSourceSaver\JWTAuth\Http\Middleware\RefreshToken::class,
|
||||
];
|
||||
}
|
||||
@@ -3,12 +3,21 @@
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\Parents\PrimaryParentUserResolver;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/** CodeIgniter route filter `auth:parent`. */
|
||||
/**
|
||||
* CodeIgniter route filter `auth:parent`.
|
||||
*
|
||||
* Allows access for any caller that ultimately maps to a primary parent
|
||||
* record: the primary parent themselves (parent role / user_type=primary),
|
||||
* a registered second parent (user_type=secondary) or an authorized user
|
||||
* (user_type=tertiary). The resolved primary parent id is stashed on the
|
||||
* request as `primary_parent_id` for controllers to consume.
|
||||
*/
|
||||
class EnsureParentProgressAccess
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
@@ -25,9 +34,23 @@ class EnsureParentProgressAccess
|
||||
->toArray();
|
||||
|
||||
if (in_array('parent', $roles, true)) {
|
||||
$request->attributes->set('primary_parent_id', (int) $user->id);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$userType = strtolower(trim((string) ($user->user_type ?? '')));
|
||||
if (in_array($userType, ['secondary', 'tertiary'], true)) {
|
||||
/** @var PrimaryParentUserResolver $resolver */
|
||||
$resolver = app(PrimaryParentUserResolver::class);
|
||||
$primaryId = $resolver->resolveFromCredentials((int) $user->id, $userType);
|
||||
if ($primaryId !== null && $primaryId > 0) {
|
||||
$request->attributes->set('primary_parent_id', $primaryId);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Forbidden.'], 403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Finance;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class ApplyExtraChargeRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'invoice_id' => ['required', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Finance;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class ChargeListRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'parent_id' => ['required', 'integer', 'min:1'],
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Finance;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class FeeFamilyBalanceRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'parent_id' => ['required', 'integer', 'min:1'],
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
'students' => ['required', 'array', 'min:1'],
|
||||
'students.*.student_id' => ['nullable', 'integer', 'min:1'],
|
||||
'students.*.firstname' => ['nullable', 'string', 'max:100'],
|
||||
'students.*.lastname' => ['nullable', 'string', 'max:100'],
|
||||
'students.*.class_section_id' => ['nullable', 'integer', 'min:1'],
|
||||
'students.*.grade' => ['nullable', 'string', 'max:20'],
|
||||
'students.*.enrollment_status' => ['nullable', 'string', 'max:50'],
|
||||
'students.*.admission_status' => ['nullable', 'string', 'max:50'],
|
||||
'students.*.discount_amount' => ['nullable', 'numeric', 'min:0'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Finance;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class FeeTuitionBreakdownRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'students' => ['required', 'array', 'min:1'],
|
||||
'students.*.student_id' => ['nullable', 'integer', 'min:1'],
|
||||
'students.*.firstname' => ['nullable', 'string', 'max:100'],
|
||||
'students.*.lastname' => ['nullable', 'string', 'max:100'],
|
||||
'students.*.class_section_id' => ['nullable', 'integer', 'min:1'],
|
||||
'students.*.grade' => ['nullable', 'string', 'max:20'],
|
||||
'students.*.enrollment_status' => ['nullable', 'string', 'max:50'],
|
||||
'students.*.admission_status' => ['nullable', 'string', 'max:50'],
|
||||
'students.*.discount_amount' => ['nullable', 'numeric', 'min:0'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Finance;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class MarkEventChargePaidRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'paid' => ['required', 'boolean'],
|
||||
'payment_id' => ['nullable', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Finance;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class StoreEventChargeRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'parent_id' => ['required', 'integer', 'min:1'],
|
||||
'student_id' => ['required', 'integer', 'min:1'],
|
||||
'event_id' => ['nullable', 'integer', 'min:1'],
|
||||
'event_name' => ['required_without:event_id', 'nullable', 'string', 'max:255'],
|
||||
'amount' => ['nullable', 'numeric', 'min:0'],
|
||||
'description' => ['nullable', 'string', 'max:2000'],
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
'semester' => ['nullable', 'string', 'max:20'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Finance;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class StoreExtraChargeRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'parent_id' => ['required', 'integer', 'min:1'],
|
||||
'title' => ['required', 'string', 'min:2', 'max:255'],
|
||||
'amount' => ['required', 'numeric', 'min:0.01'],
|
||||
'charge_type' => ['required', 'in:add,deduct'],
|
||||
'description' => ['nullable', 'string', 'max:2000'],
|
||||
'due_date' => ['nullable', 'date_format:Y-m-d'],
|
||||
'invoice_id' => ['nullable', 'integer', 'min:1'],
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
'semester' => ['nullable', 'string', 'max:20'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Promotions;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
use App\Models\StudentPromotionRecord;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AdminListPromotionsRequest extends ApiFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => ['nullable'],
|
||||
'status.*' => ['string', Rule::in(StudentPromotionRecord::ALL_STATUSES)],
|
||||
'next_school_year' => ['nullable', 'string', 'max:9'],
|
||||
'current_school_year' => ['nullable', 'string', 'max:9'],
|
||||
'parent_id' => ['nullable', 'integer', 'min:1'],
|
||||
'student_id' => ['nullable', 'integer', 'min:1'],
|
||||
'search' => ['nullable', 'string', 'max:191'],
|
||||
'parent_action_required' => ['nullable', 'boolean'],
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:200'],
|
||||
];
|
||||
}
|
||||
|
||||
public function filters(): array
|
||||
{
|
||||
$data = $this->validated();
|
||||
$status = $data['status'] ?? null;
|
||||
if (is_string($status) && $status !== '') {
|
||||
$status = [$status];
|
||||
} elseif (!is_array($status)) {
|
||||
$status = null;
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'next_school_year' => $data['next_school_year'] ?? null,
|
||||
'current_school_year' => $data['current_school_year'] ?? null,
|
||||
'parent_id' => isset($data['parent_id']) ? (int) $data['parent_id'] : null,
|
||||
'student_id' => isset($data['student_id']) ? (int) $data['student_id'] : null,
|
||||
'search' => $data['search'] ?? null,
|
||||
'parent_action_required' => isset($data['parent_action_required'])
|
||||
? (bool) $data['parent_action_required']
|
||||
: false,
|
||||
'page' => isset($data['page']) ? (int) $data['page'] : 1,
|
||||
'per_page' => isset($data['per_page']) ? (int) $data['per_page'] : 50,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Promotions;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class EvaluateEligibilityRequest extends ApiFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'scope' => ['nullable', 'string', 'in:student,class_section,school_year'],
|
||||
'student_id' => ['nullable', 'integer', 'min:1', 'required_if:scope,student'],
|
||||
'class_section_id' => ['nullable', 'integer', 'min:1', 'required_if:scope,class_section'],
|
||||
'current_school_year' => ['nullable', 'string', 'max:9'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Promotions;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class ParentEnrollmentStepRequest extends ApiFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'info_confirmed' => ['nullable', 'boolean'],
|
||||
'documents_uploaded' => ['nullable', 'boolean'],
|
||||
'agreement_accepted' => ['nullable', 'boolean'],
|
||||
'payment_completed' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
public function steps(): array
|
||||
{
|
||||
$data = $this->validated();
|
||||
$steps = [];
|
||||
foreach (['info_confirmed', 'documents_uploaded', 'agreement_accepted', 'payment_completed'] as $field) {
|
||||
if (array_key_exists($field, $data) && $data[$field] !== null) {
|
||||
$steps[$field] = (bool) $data[$field];
|
||||
}
|
||||
}
|
||||
return $steps;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Promotions;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class ParentPromotionOverviewRequest extends ApiFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'next_school_year' => ['nullable', 'string', 'max:9'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Promotions;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
use App\Models\PromotionReminderLog;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class SendReminderRequest extends ApiFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'reminder_type' => ['nullable', 'string', Rule::in(PromotionReminderLog::ALLOWED_TYPES)],
|
||||
'subject' => ['nullable', 'string', 'max:191'],
|
||||
'message' => ['nullable', 'string', 'max:5000'],
|
||||
'channels' => ['nullable', 'array'],
|
||||
'channels.*' => ['string', Rule::in(['in_app', 'email', 'sms'])],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Promotions;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class SetEnrollmentDeadlineRequest extends ApiFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'enrollment_deadline' => ['required', 'date_format:Y-m-d'],
|
||||
'apply_to' => ['nullable', 'string', 'in:single,school_year,filter'],
|
||||
'next_school_year' => ['nullable', 'string', 'max:9'],
|
||||
'promotion_ids' => ['nullable', 'array'],
|
||||
'promotion_ids.*' => ['integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Promotions;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
use App\Models\StudentPromotionRecord;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdatePromotionStatusRequest extends ApiFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => ['required', 'string', Rule::in(StudentPromotionRecord::ALL_STATUSES)],
|
||||
'force' => ['nullable', 'boolean'],
|
||||
'notes' => ['nullable', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Promotions;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class UpsertLevelProgressionRequest extends ApiFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'current_level_id' => ['nullable', 'integer', 'min:1'],
|
||||
'current_level_name' => ['required', 'string', 'max:100'],
|
||||
'next_level_id' => ['nullable', 'integer', 'min:1'],
|
||||
'next_level_name' => ['nullable', 'string', 'max:100'],
|
||||
'order_index' => ['nullable', 'integer', 'min:0', 'max:65535'],
|
||||
'is_terminal' => ['nullable', 'boolean'],
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
'notes' => ['nullable', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Fees;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ChargeActionResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return array_filter([
|
||||
'ok' => (bool) ($this->resource['ok'] ?? false),
|
||||
'charge_type' => $this->resource['charge_type'] ?? null,
|
||||
'id' => isset($this->resource['id']) ? (int) $this->resource['id'] : null,
|
||||
'parent_id' => isset($this->resource['parent_id']) ? (int) $this->resource['parent_id'] : null,
|
||||
'student_id' => isset($this->resource['student_id']) ? (int) $this->resource['student_id'] : null,
|
||||
'invoice_id' => isset($this->resource['invoice_id']) ? (int) $this->resource['invoice_id'] : null,
|
||||
'event_id' => isset($this->resource['event_id']) ? (int) $this->resource['event_id'] : null,
|
||||
'event_name' => $this->resource['event_name'] ?? null,
|
||||
'amount' => isset($this->resource['amount']) ? (float) $this->resource['amount'] : null,
|
||||
'status' => $this->resource['status'] ?? null,
|
||||
'payment_status' => $this->resource['payment_status'] ?? null,
|
||||
'participation_status' => $this->resource['participation_status'] ?? null,
|
||||
'cancellation_status' => $this->resource['cancellation_status'] ?? null,
|
||||
'account_credit_charge_id' => isset($this->resource['account_credit_charge_id'])
|
||||
? (int) $this->resource['account_credit_charge_id']
|
||||
: null,
|
||||
'message' => $this->resource['message'] ?? null,
|
||||
'error' => $this->resource['error'] ?? null,
|
||||
], fn ($value) => $value !== null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Fees;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ChargeListResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'parent_id' => (int) ($this->resource['parent_id'] ?? 0),
|
||||
'school_year' => $this->resource['school_year'] ?? null,
|
||||
'extra_charges' => $this->resource['extra_charges'] ?? [],
|
||||
'event_charges' => $this->resource['event_charges'] ?? [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Fees;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class FeeFamilyBalanceResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$charges = $this->resource['charges'] ?? [];
|
||||
$credits = $this->resource['credits'] ?? [];
|
||||
$students = $this->resource['students'] ?? [];
|
||||
|
||||
return [
|
||||
'parent_id' => (int) ($this->resource['parent_id'] ?? 0),
|
||||
'school_year' => $this->resource['school_year'] ?? null,
|
||||
'charges' => [
|
||||
'tuition' => (float) ($charges['tuition'] ?? 0),
|
||||
'extra_charges' => (float) ($charges['extra_charges'] ?? 0),
|
||||
'event_charges' => (float) ($charges['event_charges'] ?? 0),
|
||||
'late_fees' => (float) ($charges['late_fees'] ?? 0),
|
||||
'total' => (float) ($charges['total'] ?? 0),
|
||||
],
|
||||
'credits' => [
|
||||
'discounts' => (float) ($credits['discounts'] ?? 0),
|
||||
'total' => (float) ($credits['total'] ?? 0),
|
||||
],
|
||||
'payments' => (float) ($this->resource['payments'] ?? 0),
|
||||
'refunds_applied' => (float) ($this->resource['refunds_applied'] ?? 0),
|
||||
'remaining_balance' => (float) ($this->resource['remaining_balance'] ?? 0),
|
||||
'account_credit' => (float) ($this->resource['account_credit'] ?? 0),
|
||||
'students' => array_map(function (array $row): array {
|
||||
return [
|
||||
'student_id' => $row['student_id'] ?? null,
|
||||
'student_name' => $row['student_name'] ?? null,
|
||||
'grade' => $row['grade'] ?? null,
|
||||
'enrollment_status' => $row['enrollment_status'] ?? null,
|
||||
'tuition' => (float) ($row['tuition'] ?? 0),
|
||||
'extra_charges' => (float) ($row['extra_charges'] ?? 0),
|
||||
'event_charges' => (float) ($row['event_charges'] ?? 0),
|
||||
'total_charges' => (float) ($row['total_charges'] ?? 0),
|
||||
'payments_applied' => (float) ($row['payments_applied'] ?? 0),
|
||||
'credits_applied' => (float) ($row['credits_applied'] ?? 0),
|
||||
'refunds_applied' => (float) ($row['refunds_applied'] ?? 0),
|
||||
'remaining_balance' => (float) ($row['remaining_balance'] ?? 0),
|
||||
'account_credit' => (float) ($row['account_credit'] ?? 0),
|
||||
];
|
||||
}, $students),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ class FeeRefundResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$students = $this->resource['students'] ?? [];
|
||||
|
||||
return [
|
||||
'refund_amount' => (float) ($this->resource['refund_amount'] ?? 0),
|
||||
'total_paid' => (float) ($this->resource['total_paid'] ?? 0),
|
||||
@@ -16,6 +18,20 @@ class FeeRefundResource extends JsonResource
|
||||
'school_end_date' => $this->resource['school_end_date'] ?? null,
|
||||
'weeks_of_study' => (float) ($this->resource['weeks_of_study'] ?? 0),
|
||||
'withdrawn_students' => (int) ($this->resource['withdrawn_students'] ?? 0),
|
||||
'students' => array_map(function (array $row): array {
|
||||
return [
|
||||
'student_id' => $row['student_id'] ?? null,
|
||||
'grade' => $row['grade'] ?? null,
|
||||
'grade_level' => $row['grade_level'] ?? null,
|
||||
'fee_category' => $row['fee_category'] ?? null,
|
||||
'tuition_fee' => (float) ($row['tuition_fee'] ?? 0),
|
||||
'withdrawal_date' => $row['withdrawal_date'] ?? null,
|
||||
'weeks_remaining' => (int) ($row['weeks_remaining'] ?? 0),
|
||||
'refund_amount' => (float) ($row['refund_amount'] ?? 0),
|
||||
'eligible' => (bool) ($row['eligible'] ?? false),
|
||||
'reason' => $row['reason'] ?? null,
|
||||
];
|
||||
}, $students),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Fees;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class FeeTuitionBreakdownResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$students = $this->resource['students'] ?? [];
|
||||
$fees = $this->resource['fees'] ?? [];
|
||||
|
||||
return [
|
||||
'school_year' => $this->resource['school_year'] ?? null,
|
||||
'family_total' => (float) ($this->resource['family_total'] ?? 0),
|
||||
'billable_count' => (int) ($this->resource['billable_count'] ?? 0),
|
||||
'non_billable_count' => (int) ($this->resource['non_billable_count'] ?? 0),
|
||||
'fees' => [
|
||||
'first_student_fee' => (float) ($fees['first_student_fee'] ?? 0),
|
||||
'second_student_fee' => (float) ($fees['second_student_fee'] ?? 0),
|
||||
'youth_fee' => (float) ($fees['youth_fee'] ?? 0),
|
||||
],
|
||||
'students' => array_map(function (array $row): array {
|
||||
return [
|
||||
'student_id' => $row['student_id'] ?? null,
|
||||
'student_name' => $row['student_name'] ?? null,
|
||||
'grade' => $row['grade'] ?? null,
|
||||
'grade_level' => $row['grade_level'] ?? null,
|
||||
'enrollment_status' => $row['enrollment_status'] ?? null,
|
||||
'admission_status' => $row['admission_status'] ?? null,
|
||||
'fee_category' => $row['fee_category'] ?? null,
|
||||
'tuition_fee' => (float) ($row['tuition_fee'] ?? 0),
|
||||
'discount_amount' => (float) ($row['discount_amount'] ?? 0),
|
||||
'final_tuition_amount' => (float) ($row['final_tuition_amount'] ?? 0),
|
||||
];
|
||||
}, $students),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Promotions;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class LevelProgressionResource extends JsonResource
|
||||
{
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) ($this->id ?? 0),
|
||||
'current_level_id' => $this->current_level_id !== null ? (int) $this->current_level_id : null,
|
||||
'current_level_name' => (string) ($this->current_level_name ?? ''),
|
||||
'next_level_id' => $this->next_level_id !== null ? (int) $this->next_level_id : null,
|
||||
'next_level_name' => $this->next_level_name,
|
||||
'order_index' => (int) ($this->order_index ?? 0),
|
||||
'is_terminal' => (bool) $this->is_terminal,
|
||||
'is_active' => (bool) $this->is_active,
|
||||
'notes' => $this->notes,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Promotions;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class PromotionAuditLogResource extends JsonResource
|
||||
{
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) ($this->id ?? 0),
|
||||
'promotion_id' => $this->promotion_id ? (int) $this->promotion_id : null,
|
||||
'student_id' => $this->student_id ? (int) $this->student_id : null,
|
||||
'user_id' => $this->user_id ? (int) $this->user_id : null,
|
||||
'action_type' => $this->action_type,
|
||||
'field' => $this->field,
|
||||
'old_value' => $this->old_value,
|
||||
'new_value' => $this->new_value,
|
||||
'notes' => $this->notes,
|
||||
'performed_at' => optional($this->performed_at)->toDateTimeString(),
|
||||
'created_at' => optional($this->created_at)->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Promotions;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class PromotionReminderResource extends JsonResource
|
||||
{
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) ($this->id ?? 0),
|
||||
'promotion_id' => (int) ($this->promotion_id ?? 0),
|
||||
'student_id' => $this->student_id ? (int) $this->student_id : null,
|
||||
'parent_id' => $this->parent_id ? (int) $this->parent_id : null,
|
||||
'reminder_type' => $this->reminder_type,
|
||||
'channel' => $this->channel,
|
||||
'subject' => $this->subject,
|
||||
'message' => $this->message,
|
||||
'sent_at' => optional($this->sent_at)->toDateTimeString(),
|
||||
'sent_by' => $this->sent_by ? (int) $this->sent_by : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Promotions;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* Admin/list shape for `student_promotion_records`. Accepts either an
|
||||
* Eloquent model or an associative array (so list endpoints can
|
||||
* decorate the row with student/parent context once and pass it
|
||||
* through unchanged).
|
||||
*/
|
||||
class StudentPromotionResource extends JsonResource
|
||||
{
|
||||
public function toArray($request): array
|
||||
{
|
||||
$r = $this->resource;
|
||||
$get = static function (string $key, $default = null) use ($r) {
|
||||
if (is_array($r)) {
|
||||
return $r[$key] ?? $default;
|
||||
}
|
||||
return $r->{$key} ?? $default;
|
||||
};
|
||||
|
||||
return [
|
||||
'promotion_id' => (int) ($get('promotion_id') ?? 0),
|
||||
'student_id' => (int) ($get('student_id') ?? 0),
|
||||
'student_name' => $get('student_name'),
|
||||
'school_id' => $get('school_id'),
|
||||
'parent_id' => $get('parent_id') ? (int) $get('parent_id') : null,
|
||||
'parent_name' => $get('parent_name'),
|
||||
'parent_email' => $get('parent_email'),
|
||||
'current_school_year' => $get('current_school_year'),
|
||||
'next_school_year' => $get('next_school_year'),
|
||||
'current_level' => $get('current_level') ?? $get('current_level_name'),
|
||||
'promoted_level' => $get('promoted_level') ?? $get('promoted_level_name'),
|
||||
'promotion_status' => $get('promotion_status'),
|
||||
'enrollment_status' => $get('enrollment_status'),
|
||||
'enrollment_deadline' => $get('enrollment_deadline'),
|
||||
'parent_notified_at' => $get('parent_notified_at'),
|
||||
'enrollment_started_at' => $get('enrollment_started_at'),
|
||||
'enrollment_completed_at' => $get('enrollment_completed_at'),
|
||||
'promotion_finalized_at' => $get('promotion_finalized_at'),
|
||||
'final_average' => $get('final_average') !== null ? (float) $get('final_average') : null,
|
||||
'passed_current_level' => $get('passed_current_level'),
|
||||
'checklist' => $get('checklist'),
|
||||
'eligibility_notes' => $get('eligibility_notes'),
|
||||
'enrollment_id' => $get('enrollment_id') ? (int) $get('enrollment_id') : null,
|
||||
'updated_by' => $get('updated_by') ? (int) $get('updated_by') : null,
|
||||
'updated_at' => $get('updated_at'),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user