Skip to content

Instantly share code, notes, and snippets.

@fredbradley
Created September 30, 2025 13:46
Show Gist options
  • Save fredbradley/33cb9fec939c009643122a94b09f368e to your computer and use it in GitHub Desktop.
Save fredbradley/33cb9fec939c009643122a94b09f368e to your computer and use it in GitHub Desktop.
<?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' => [],
];
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment