Skip to content

Instantly share code, notes, and snippets.

@fredbradley
Created September 30, 2025 13:46
Show Gist options
  • Select an option

  • Save fredbradley/33cb9fec939c009643122a94b09f368e to your computer and use it in GitHub Desktop.

Select an option

Save fredbradley/33cb9fec939c009643122a94b09f368e to your computer and use it in GitHub Desktop.

Revisions

  1. fredbradley created this gist Sep 30, 2025.
    346 changes: 346 additions & 0 deletions IsamsRegistrationService.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,346 @@
    <?php

    namespace App\Services;

    use AllowDynamicProperties;
    use App\Exceptions\UnableToFindRegistrationDataInTheFuture;
    use App\Exceptions\UnknownRegistrationType;
    use App\Logic\ConductAndRewards\CreditsAndDemerits\Credit;
    use App\Models\House;
    use App\Models\Pupil;
    use App\Models\School;
    use Carbon\CarbonInterface;
    use Exception;
    use GuzzleHttp\Exception\ClientException;
    use GuzzleHttp\Exception\ConnectException;
    use GuzzleHttp\Exception\GuzzleException;
    use GuzzleHttp\Exception\ServerException;
    use Illuminate\Support\Collection;
    use Illuminate\Support\Facades\Log;
    use Illuminate\Support\ItemNotFoundException;
    use spkm\isams\Controllers\RoughAndReadyController;
    use spkm\isams\Exceptions\IsamsInstanceNotFound;
    use stdClass;

    #[AllowDynamicProperties] class IsamsRegistrationService
    {
    public Collection $absenceCodes;

    public Collection $presentCodes;

    public ?object $registrationPeriods = null;

    private RoughAndReadyController $controller;

    /**
    * @throws GuzzleException
    * @throws IsamsInstanceNotFound
    */
    public function __construct()
    {
    $this->controller = new RoughAndReadyController;
    $this->setPresentCodes();
    $this->setAbsenceCodes();
    }

    /**
    * @throws GuzzleException
    * @throws IsamsInstanceNotFound
    */
    public function setPresentCodes(): void
    {
    try {
    $this->presentCodes = collect($this->controller->get('registration/presentcodes')->presentCodes);
    } catch (ClientException $exception) {
    if ($exception->getCode() === 404) {
    throw new IsamsInstanceNotFound(
    'ISAMS gave us a 404 when trying to get present codes.',
    404,
    $exception
    );
    }
    session()->flash('alert-danger', 'Could not retrieve data from ISAMS. Check connection.');
    } catch (ConnectException $exception) {
    session()->flash('alert-danger', 'Could not connect to ISAMS. Check connection.');
    $this->presentCodes = collect([]);
    }
    }

    /**
    * @throws GuzzleException
    */
    public function setAbsenceCodes(): void
    {
    try {
    $this->absenceCodes = collect($this->controller->get('registration/absencecodes')->absenceCodes);
    } catch (ClientException $exception) {
    session()->flash('alert-danger', 'Could not retrieve data from ISAMS. Check connection.');
    } catch (ConnectException $exception) {
    session()->flash('alert-danger', 'Could not connect to ISAMS. Check connection.');
    $this->absenceCodes = collect([]);
    }
    }

    /**
    * @deprecated This method is deprecated and will be removed in a future version.
    *
    * We no longer need to define a school to use this service
    */
    public function withSchool(School $school): IsamsRegistrationService
    {
    return $this;
    }

    /**
    * @throws GuzzleException
    */
    public function hasDemeritBeenGiven(House $house, CarbonInterface $date, string $type, Pupil $pupil): bool
    {
    $result = collect($this->controller->get(
    'rewardsAndConduct/students/'.$pupil->mis_id.'/rewards', [
    // can't figure out how to get $filter to work as per API docs
    ]
    )->rewards)->filter(function ($reward) {
    return $reward->date === now()->format('Y-m-d');
    })->filter(function ($reward) use ($type) {
    $str = match ($type) {
    'Am' => 'Late for Morning Callover',
    'Pm' => 'Late for Lunchtime Callover',
    default => throw new Exception('Invalid callover type')
    };

    return str_contains($reward->description, $str);
    });

    return $result->count() > 0;
    }

    /**
    * @throws Exception
    * @throws GuzzleException
    */
    public function giveLateCalloverHouseDemerit(Pupil $pupil, CarbonInterface $date, string $type)
    {
    $this->sanitizeType($type);
    $credit = new Credit(
    $pupil,
    auth()->user()->staff,
    'House Demerit'
    );

    $description = match ($type) {
    'Am' => 'Late for Morning Callover '.$date->format('d/m/Y'),
    'Pm' => 'Late for Lunchtime Callover '.$date->format('d/m/Y')
    };
    $credit
    ->setCategory('Late for callover / sport')
    ->setDescription($description);

    return $credit->post();
    }

    /**
    * This works like a validator, rather than setting a santized type, it throws an exception if the type is not valid.
    *
    * @throws UnknownRegistrationType
    */
    public function sanitizeType(string $type): void
    {
    $type = ucfirst($type);

    if ($type !== 'Am' && $type !== 'Pm' && $type !== 'Other' && $type !== 'Ev') {
    throw new UnknownRegistrationType('Unknown Registration Type. Should be either "Am" or "Pm" or "Ev".');
    }
    }

    /**
    * @return mixed
    *
    * @throws GuzzleException
    */
    public function registerPupil(
    string $misId,
    int $registrationPeriodId,
    bool $isPresent,
    int $absenceCodeId
    ) {
    if ($isPresent) {
    $isLate = false;
    $data = compact('isPresent', 'isLate');
    } else {
    $data = compact('absenceCodeId', 'isPresent');
    }

    return $this->updatePupilRegistrationStatus($misId, $registrationPeriodId, $data);
    }

    /**
    * @throws GuzzleException
    */
    public function updatePupilRegistrationStatus(string $misId, int $registrationPeriodId, array $data)
    {
    return $this->controller->request(
    'PUT',
    'registration/register/'.$registrationPeriodId.'/students/'.$misId, [], $data,
    );
    }

    /**
    * @throws GuzzleException
    */
    public function getPupilRegistrationStatus(string $misId, int $registrationPeriodId): object
    {
    try {
    return $this->controller->request(
    'GET',
    'registration/register/'.$registrationPeriodId.'/students/'.$misId
    );
    } catch (ClientException $exception) {
    if ($exception->getCode() === 404) {
    session()->flash('alert-warning', 'Registration status does not exist.');

    return (object) []; // TODO: should this return an HTTP 404 exception instead? Think about knock on effects...
    } else {
    throw $exception;
    }
    }
    }

    /**
    * @throws UnableToFindRegistrationDataInTheFuture|GuzzleException
    */
    public function getCalloverAbsentees(House $house, CarbonInterface $date, string $type): Collection
    {
    $housePupils = $house->pupils()->current()->get();

    return $this->prepareCalloverData($date, $type)
    ->whereIn('schoolId', $housePupils->pluck('mis_id'))
    ->map(function ($pupil) use ($housePupils) {
    $pupil->absent = optional($this->absenceCodes->where('id',
    $pupil->absenceCodeId)->first())->name;
    $pupil->present = optional($this->presentCodes->where('id',
    $pupil->presentCodeId)->first())->name;
    $pupil->name = $housePupils->where('mis_id', $pupil->schoolId)->first()->name;

    return $pupil;
    })->whereNotNull('absent');
    }

    /**
    * @throws UnableToFindRegistrationDataInTheFuture
    * @throws GuzzleException
    * @throws Exception
    */
    public function prepareCalloverData(CarbonInterface $date, string $type, ?School $school = null): Collection
    {
    if (is_null($school)) {
    $school = defaultIsamsInstitution();
    }
    if ($date->isAfter(now())) {
    throw new UnableToFindRegistrationDataInTheFuture('Cannot find registration data for '.$date->format('Y-m-d').' when it is currently '.now()->format('Y-m-d'),
    400);
    }
    if ($type === 'Latest') {
    $type = $date->isToday() ? (now()->hour < 13 ? 'Am' : 'Pm') : 'Pm';
    }

    $this->sanitizeType($type);

    try {
    $callover = $this->getRegistrationPeriod($date, $type, $school);
    if (is_null($callover)) {
    throw new ItemNotFoundException('Callover Period not found');
    }
    } catch (ItemNotFoundException $exception) {
    session()->flash('Callover Period not found');
    Log::warning('Callover Period not found');

    return collect();
    }
    try {
    $result = $this->controller->get('registration/register/'.$callover->id, [
    'pageSize' => 800,
    ]);
    } catch (ServerException $exception) {
    if (str_contains($exception->getMessage(),
    'Registration Statuses have not been generated for this registration')) {
    session()->flash('alert-warning', 'Tried and failed to get registration data that does not yet exist');
    Log::warning('Tried and failed to get registration data that does not yet exist');

    return collect();
    }
    throw $exception;
    }

    return collect($result->registrationStatuses);
    }

    /**
    * @throws Exception
    * @throws GuzzleException
    */
    public function getRegistrationPeriod(CarbonInterface $date, string $type, ?School $school = null): ?object
    {
    if ($school === null) {
    $school = defaultIsamsInstitution();
    }

    if ($type === 'Latest') {
    $type = now()->hour < 13 ? 'Am' : 'Pm';
    }

    $this->sanitizeType($type);

    $periods = $this->getRegistrationPeriods([
    'startDate' => $date->format('Y-m-d'),
    'endDate' => $date->format('Y-m-d'),
    ]);
    if (strtolower($type) === 'ev') {
    $type = 'Other';
    }

    return collect($periods->registrationPeriods)
    ->where('registrationType', '=', $type)
    ->filter(function (stdClass $callover) use ($school) {
    return $callover->divisions[0]->id === $school->division_id; // Senior School
    })
    ->first();
    }

    /**
    * @throws GuzzleException
    */
    public function getRegistrationPeriods(array $options = []): mixed
    {
    try {
    $this->registrationPeriods = $this->controller->get('registration/periods', $options);
    } catch (ClientException $exception) {
    if ($exception->getCode() === 422) {
    $this->registrationPeriods = (object) [
    'registrationPeriods' => [],
    ];
    session()->flash('alert-warning', 'No registration periods found for this date.');
    }
    } catch (Exception $exception) {
    session()->flash('alert-danger', 'Could not retrieve data from ISAMS. Check connection.');
    $this->registrationPeriods = (object) [
    'registrationPeriods' => [],
    ];
    }

    return $this->registrationPeriods;
    }

    public function getPeriodRegistrationStatuses(int $periodId, array $options)
    {
    try {
    return $this->controller->get('registration/register/'.$periodId, $options);
    } catch (Exception $exception) {
    session()->flash('alert-danger', 'Could not retrieve data from ISAMS. Check connection.');

    return (object) [
    'registrationStatuses' => [],
    ];
    }
    }
    }