Commit cf6c672e authored by Jacob Priddy's avatar Jacob Priddy 👌

Refactoring on the status response

Make it Schedule Events and have it return only within given date range
parent 885774d8
......@@ -219,7 +219,6 @@ return [
// DoorUser
Source\UseCases\DoorUser\UserDoorAccess\UserDoorAccessUseCaseServiceProvider::class,
Source\UseCases\DoorUser\DoorUserScheduleEventAccess\DoorUserScheduleEventAccessUseCaseServiceProvider::class,
// Groups
Source\UseCases\Groups\GetGroup\GetGroupUseCaseServiceProvider::class,
......@@ -244,7 +243,7 @@ return [
Source\UseCases\Door\Access\AccessUseCaseServiceProvider::class,
Source\UseCases\Door\Authenticate\AuthenticateUseCaseServiceProvider::class,
Source\UseCases\Door\UpdateBinary\UpdateBinaryUseCaseServiceProvider::class,
\Source\UseCases\Door\ScheduleEvents\ScheduleEventsUseCaseServiceProvider::class,
Source\UseCases\Door\ScheduleEvents\ScheduleEventsUseCaseServiceProvider::class,
// Doors
Source\UseCases\Doors\GetDoor\GetDoorUseCaseServiceProvider::class,
......
......@@ -201,7 +201,7 @@ class Schedule
*/
public function addDuration(Carbon $carbon): Carbon
{
return $carbon->addRealSeconds($this->duration);
return $carbon->clone()->addRealSeconds($this->duration);
}
/**
......@@ -210,7 +210,7 @@ class Schedule
*/
public function subDuration(Carbon $carbon): Carbon
{
return $carbon->subRealSeconds($this->duration);
return $carbon->clone()->subRealSeconds($this->duration);
}
/**
......
......@@ -49,4 +49,20 @@ class ScheduleEvent
{
return $other->getBegin()->diffInRealSeconds($this->getBegin(), false);
}
/**
* @param \Carbon\Carbon $begin
*/
public function setBegin(Carbon $begin): void
{
$this->begin = $begin;
}
/**
* @param \Carbon\Carbon $end
*/
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
}
......@@ -22,6 +22,22 @@ class DatabaseDoorScheduleRepository implements DoorScheduleRepository
$this->db = $db;
}
protected function createScheduleFromObject($schedule): Schedule
{
return new Schedule(
$schedule->id,
$schedule->group_id,
$schedule->type,
$schedule->rset,
$schedule->duration,
$schedule->description,
$this->castToCarbon($schedule->start),
$this->castToCarbon($schedule->end),
$this->castToCarbon($schedule->created_at),
$this->castToCarbon($schedule->updated_at)
);
}
/**
* @inheritDoc
*/
......@@ -41,64 +57,59 @@ QUERY;
]);
return array_map(function ($schedule) {
return new Schedule(
$schedule->id,
$schedule->group_id,
$schedule->type,
$schedule->rset,
$schedule->duration,
$schedule->description,
$this->castToCarbon($schedule->start),
$this->castToCarbon($schedule->end),
$this->castToCarbon($schedule->created_at),
$this->castToCarbon($schedule->updated_at)
);
$this->createScheduleFromObject($schedule);
}, $schedules);
}
/**
* @inheritDoc
*/
public function getSchedulesForDoorBetween(string $doorId, Carbon $begin, Carbon $end, ?int $type = Schedule::TYPE_OPEN_MODE): array
public function getSchedulesForDoorBetween(string $doorId, Carbon $begin, Carbon $end, int $type = Schedule::TYPE_OPEN_MODE): array
{
if ($type !== null) {
$query = <<<QUERY
$query = <<<QUERY
select S.id, S.group_id, S.type, S.rset, S.start, S.end, S.duration, S.description, S.created_at, S.updated_at
FROM schedules as S
INNER JOIN door_group AS DG ON DG.group_id = S.group_id AND DG.door_id = :DOOR_ID
WHERE S.type = :TYPE AND (((:BEGIN, :END) OVERLAPS (s.start, S.end))
OR (S.start < :END AND S.end IS NULL))
QUERY;
} else {
$query = <<<QUERY
$schedules = $this->db->select($query, [
':DOOR_ID' => $this->castToInt($doorId),
':TYPE' => $this->castToInt($type),
':BEGIN' => $begin,
':END' => $end,
]);
return array_map(function ($schedule) {
$this->createScheduleFromObject($schedule);
}, $schedules);
}
/**
* @inheritDoc
*/
public function getSchedulesForDoorAndUserBetween(string $doorId, string $userId, Carbon $begin, Carbon $end): array
{
$query = <<<QUERY
select S.id, S.group_id, S.type, S.rset, S.start, S.end, S.duration, S.description, S.created_at, S.updated_at
FROM schedules as S
INNER JOIN door_group AS DG ON DG.group_id = S.group_id AND DG.door_id = :DOOR_ID
WHERE (((:BEGIN, :END) OVERLAPS (s.start, S.end))
INNER JOIN group_user as GU ON GU.group_id = S.group_id AND GU.user_id = :USER_ID
WHERE S.type = :TYPE (((:BEGIN, :END) OVERLAPS (s.start, S.end))
OR (S.start < :END AND S.end IS NULL))
QUERY;
}
$schedules = $this->db->select($query, [
':DOOR_ID' => $this->castToInt($doorId),
':TYPE' => $this->castToInt($type),
':USER_ID' => $this->castToInt($userId),
':TYPE' => $this->castToInt(Schedule::TYPE_USER_ACCESS),
':BEGIN' => $begin,
':END' => $end,
]);
return array_map(function ($schedule) {
return new Schedule(
$schedule->id,
$schedule->group_id,
$schedule->type,
$schedule->rset,
$schedule->duration,
$schedule->description,
$this->castToCarbon($schedule->start),
$this->castToCarbon($schedule->end),
$this->castToCarbon($schedule->created_at),
$this->castToCarbon($schedule->updated_at)
);
$this->createScheduleFromObject($schedule);
}, $schedules);
}
}
......@@ -21,5 +21,14 @@ interface DoorScheduleRepository
* @param int|null $type
* @return \Source\Entities\Schedule[]
*/
public function getSchedulesForDoorBetween(string $doorId, Carbon $begin, Carbon $end, ?int $type = Schedule::TYPE_OPEN_MODE): array;
public function getSchedulesForDoorBetween(string $doorId, Carbon $begin, Carbon $end, int $type = Schedule::TYPE_OPEN_MODE): array;
/**
* @param string $doorId
* @param string $userId
* @param \Carbon\Carbon $begin
* @param \Carbon\Carbon $end
* @return \Source\Entities\Schedule[]
*/
public function getSchedulesForDoorAndUserBetween(string $doorId, string $userId, Carbon $begin, Carbon $end): array;
}
......@@ -37,7 +37,19 @@ class InMemoryDoorScheduleRepository implements DoorScheduleRepository
/**
* @inheritDoc
*/
public function getSchedulesForDoorBetween(string $doorId, Carbon $begin, Carbon $end, ?int $type = Schedule::TYPE_OPEN_MODE): array
public function getSchedulesForDoorBetween(string $doorId, Carbon $begin, Carbon $end, int $type = Schedule::TYPE_OPEN_MODE): array
{
if (!isset($this->doorScheduleMap[$doorId])) {
return [];
}
return $this->doorScheduleMap[$doorId];
}
/**
* @inheritDoc
*/
public function getSchedulesForDoorAndUserBetween(string $doorId, string $userId, Carbon $begin, Carbon $end): array
{
if (!isset($this->doorScheduleMap[$doorId])) {
return [];
......
<?php
namespace Source\UseCases\Door\ScheduleEvents;
use Carbon\Carbon;
use Source\Entities\Override;
use Source\Entities\ScheduleEvent;
trait ScheduleEventOverrideMerge
{
/**
* @param \Carbon\Carbon $start
* @param \Carbon\Carbon $end
* @param \Source\Entities\Override|null $override
* @return \Source\Entities\ScheduleEvent[]
*/
protected function mergeOverrideAndScheduleEvent(Carbon $start, Carbon $end, ?Override $override): array
{
/*
* Parsing of schedules
*
* schedule no overlapping override
* - schedule encloses override
* - break schedule into two events
* - start of schedule to start of override
* - end of override to end of schedule
* - Don't intersect
* - add schedule
* schedule overlaps with override
* - schedule begin and end overlap with override
* - ignore
* - schedule begin overlaps with override
* - end of override to end of schedule
* - schedule end overlaps with override
* - beginning of schedule to beginning of override
*/
if ($override && $override->hasTypeOf(Override::TYPE_LOCKED)) {
// If the beginning of the schedule overlaps with the override
$overlapBegin = $override->isActiveForDate($start);
// If the end of the schedule overlaps with the override
$overlapEnd = $override->isActiveForDate($end);
if (!$overlapBegin && !$overlapEnd) {
if ($start->isBefore($override->getStart()) && $end->isAfter($override->getEnd())) {
// Override is enclosed by the event, break up the open event
return [
new ScheduleEvent($start, $override->getStart()),
new ScheduleEvent($override->getEnd(), $end),
];
}
// override does not overlap with event add whole event range
return [new ScheduleEvent($start, $end)];
}
if ($overlapBegin && $overlapEnd) {
// Override envelops the event, ignore the event
return [];
}
if ($overlapBegin) {
return [new ScheduleEvent($override->getEnd(), $end)];
}
return [new ScheduleEvent($start, $override->getStart())];
}
// No override to worry about, add the event
return [new ScheduleEvent($start, $end)];
}
}
......@@ -13,6 +13,8 @@ use Source\Gateways\RecurrenceSet\RecurrenceSetRepository;
class ScheduleEvents implements ScheduleEventsUseCase
{
use ScheduleEventOverrideMerge;
/**
* @var \Source\Gateways\Overrides\OverridesRepository
*/
......@@ -21,7 +23,7 @@ class ScheduleEvents implements ScheduleEventsUseCase
/**
* @var \Source\Gateways\DoorSchedule\DoorScheduleRepository
*/
protected DoorScheduleRepository $schedules;
protected DoorScheduleRepository $doorSchedules;
/**
* @var \Source\Gateways\RecurrenceSet\RecurrenceSetRepository
......@@ -32,25 +34,30 @@ class ScheduleEvents implements ScheduleEventsUseCase
* The only things that can change the status of a door is a schedule or an override
* So we look at schedules and overrides to get the status events.
*
* @param \Source\Gateways\DoorSchedule\DoorScheduleRepository $schedules
* @param \Source\Gateways\DoorSchedule\DoorScheduleRepository $doorSchedules
* @param \Source\Gateways\Overrides\OverridesRepository $overrides
* @param \Source\Gateways\RecurrenceSet\RecurrenceSetRepository $rset
*/
public function __construct(
DoorScheduleRepository $schedules,
DoorScheduleRepository $doorSchedules,
OverridesRepository $overrides,
RecurrenceSetRepository $rset
) {
$this->overrides = $overrides;
$this->schedules = $schedules;
$this->doorSchedules = $doorSchedules;
$this->rset = $rset;
}
/**
* @inheritDoc
*/
public function getStatusForDoor(string $doorId, ?string $userId, Carbon $begin, Carbon $end, Presenter $presenter): void
{
public function getStatusForDoor(
string $doorId,
?string $userId,
Carbon $begin,
Carbon $end,
Presenter $presenter
): void {
/*
* Ignore override overlaps, allow override edits
* only take latest override
......@@ -58,27 +65,15 @@ class ScheduleEvents implements ScheduleEventsUseCase
*
* Only one override can be active at a time
*/
/*
* Parsing of schedules
*
* schedule no overlapping override
* - schedule encloses override
* - break schedule into two events
* - start of schedule to start of override
* - end of override to end of schedule
* - Don't intersect
* - add schedule
* schedule overlaps with override
* - schedule begin and end overlap with override
* - ignore
* - schedule begin overlaps with override
* - end of override to end of schedule
* - schedule end overlaps with override
* - beginning of schedule to beginning of override
*/
$override = $this->overrides->activeOverrideForDoorBetween($doorId, $begin, $end);
$openSchedules = $this->schedules->getSchedulesForDoorBetween($doorId, $begin, $end, Schedule::TYPE_OPEN_MODE);
$openSchedules = $this->doorSchedules->getSchedulesForDoorBetween($doorId, $begin, $end, Schedule::TYPE_OPEN_MODE);
$userSchedules = [];
if ($userId !== null) {
$userSchedules = $this->doorSchedules->getSchedulesForDoorAndUserBetween($doorId, $userId, $begin, $end);
}
/** @var Schedule[] $schedules */
$schedules = array_merge($openSchedules, $userSchedules);
$response = new ResponseModel();
......@@ -86,7 +81,8 @@ class ScheduleEvents implements ScheduleEventsUseCase
$response->addOpenEvent(new ScheduleEvent($override->getStart(), $override->getEnd()));
}
foreach ($openSchedules as $schedule) {
// Get all open schedule times in the specified range.
foreach ($schedules as $schedule) {
try {
$this->rset->parse($schedule->getRset());
} catch (InvalidArgumentException $e) {
......@@ -97,40 +93,26 @@ class ScheduleEvents implements ScheduleEventsUseCase
$b = $schedule->subDuration($begin);
foreach ($this->rset->occurrencesBetween($b, $end) as $eventStart) {
$eventEnd = $schedule->addDuration($eventStart->clone());
$override = $this->overrides->activeOverrideForDoorBetween($doorId, $eventStart, $eventEnd);
if ($override && $override->hasTypeOf(Override::TYPE_LOCKED)) {
// If the beginning of the schedule overlaps with the override
$overlapBegin = $override->isActiveForDate($eventStart);
// If the end of the schedule overlaps with the override
$overlapEnd = $override->isActiveForDate($eventEnd);
if (!$overlapBegin && !$overlapEnd) {
if ($eventStart->isBefore($override->getStart()) && $eventEnd->isAfter($override->getEnd())) {
// Override is enclosed by the event, break up the open event
$response->addOpenEvent(new ScheduleEvent($eventStart, $override->getStart()));
$response->addOpenEvent(new ScheduleEvent($override->getEnd(), $eventEnd));
continue;
}
// override does not overlap with event add whole event range
$response->addOpenEvent(new ScheduleEvent($eventStart, $eventEnd));
continue;
$eventEnd = $schedule->addDuration($eventStart);
foreach ($this->mergeOverrideAndScheduleEvent($eventStart, $eventEnd, $override) as $scheduleEvent) {
// Lock to be beginning and end date times requested.
if ($scheduleEvent->getBegin() < $begin) {
$scheduleEvent->setBegin($begin);
}
if ($overlapBegin && $overlapEnd) {
// Override envelops the event, ignore it
continue;
if ($scheduleEvent->getEnd() > $end) {
$scheduleEvent->setEnd($end);
}
if ($overlapBegin) {
$response->addOpenEvent(new ScheduleEvent($override->getEnd(), $eventEnd));
// Skip if maybe it was split and the split event is outside the requested date range.
if ($scheduleEvent->getBegin() > $end || $scheduleEvent->getEnd() < $begin) {
continue;
}
$response->addOpenEvent(new ScheduleEvent($eventStart, $override->getStart()));
} else {
// No override to worry about, add the open event.
$response->addOpenEvent(new ScheduleEvent($eventStart, $eventEnd));
if ($schedule->hasTypeOf(Schedule::TYPE_OPEN_MODE)) {
$response->addOpenEvent($scheduleEvent);
} else if ($schedule->hasTypeOf(Schedule::TYPE_USER_ACCESS)) {
$response->addUserEvent($scheduleEvent);
}
}
}
}
......
......@@ -18,6 +18,14 @@ class APIPresenter extends BasePresenter implements Presenter
}
}
/**
* @return bool
*/
public function hasError(): bool
{
return (bool)($this->viewModel['message'] ?? false);
}
/** @inheritDoc */
public function getViewModel(): array
{
......
<?php
namespace Tests\Unit\Source\UseCases\Door\StatusResponse;
namespace Tests\Unit\Source\UseCases\Door\ScheduleEvents;
use Carbon\Carbon;
use Source\Entities\Override;
......@@ -12,8 +12,9 @@ use Source\UseCases\Door\ScheduleEvents\ResponseModel;
use Source\UseCases\Door\ScheduleEvents\ScheduleEvents;
use Source\Gateways\Overrides\InMemoryOverridesRepository;
use Source\Gateways\DoorSchedule\InMemoryDoorScheduleRepository;
use Tests\Unit\Source\UseCases\Door\StatusResponse\PresenterStub;
class StatusResponseTest extends TestCase
class ScheduleEventsTest extends TestCase
{
/**
* @var \Source\Gateways\DoorSchedule\InMemoryDoorScheduleRepository
......@@ -58,7 +59,7 @@ class StatusResponseTest extends TestCase
protected function handleTest(string $doorId, Carbon $begin, Carbon $end): void
{
$this->useCase->getStatusForDoor($doorId, null, $begin, $end, $this->presenter);
$this->useCase->getStatusForDoor($doorId, null, $begin->clone(), $end->clone(), $this->presenter);
$this->response = $this->presenter->response;
}
......@@ -76,6 +77,7 @@ class StatusResponseTest extends TestCase
/**
* @test
* @throws \Source\Exceptions\EntityNotFoundException
*/
public function it_adds_override_events(): void
{
......@@ -109,17 +111,22 @@ class StatusResponseTest extends TestCase
1,
Schedule::TYPE_OPEN_MODE,
'',
1234,
123,
'desc',
Carbon::now()->addSeconds(10)
));
$this->handleTest('1', Carbon::now(), Carbon::now()->addMinutes(10));
$this->assertEquals($this->rset->occurrences[0]->clone()->addRealSeconds(1234), $this->response->getOpenEvents()[0]->getEnd());
$this->assertCount(1, $this->response->getOpenEvents());
$this->assertEquals(
$this->rset->occurrences[0]->clone()->addRealSeconds(123),
$this->response->getOpenEvents()[0]->getEnd()
);
}
/**
* @test
* @throws \Source\Exceptions\EntityNotFoundException
*/
public function it_chops_a_schedule_in_two_when_override_is_in_the_middle(): void
{
......@@ -148,9 +155,10 @@ class StatusResponseTest extends TestCase
Carbon::now()->addSeconds(10)
));
$this->handleTest('1', Carbon::now(), Carbon::now()->addMinutes(20));
$this->handleTest('1', Carbon::now()->subMinute(), Carbon::now()->addMinutes(30));
$this->assertCount(2, $this->response->getOpenEvents());
$this->assertEquals($this->rset->occurrences[0], $this->response->getOpenEvents()[0]->getBegin());
$this->assertEquals($o->getStart(), $this->response->getOpenEvents()[0]->getEnd());
$this->assertEquals($o->getEnd(), $this->response->getOpenEvents()[1]->getBegin());
......@@ -160,22 +168,14 @@ class StatusResponseTest extends TestCase
/**
* @test
*/
public function it_gets_overrides_outside_of_date(): void
public function it_truncates_events_outside_of_date(): void
{
$start = Carbon::now();
$end = Carbon::now()->addSeconds(20);
$this->rset->occurrences = [
Carbon::now()
$start->clone()->subSeconds(5),
];
$this->overrides->addOverride(new Override(
1,
'',
1,
1,
Override::TYPE_LOCKED,
Carbon::now()->addMinute(),
Carbon::now()->addMinutes(10)
));
$this->doorSchedules->attachScheduleToDoor('1', new Schedule(
1,
1,
......@@ -184,17 +184,19 @@ class StatusResponseTest extends TestCase
// 20 minutes
60 * 20,
'desc',
Carbon::now()->addSeconds(10)
Carbon::now()->subSeconds(10)
));
$this->handleTest('1', Carbon::now(), Carbon::now()->addSeconds(20));
$this->handleTest('1', $start, $end);
// It being split means it sees the override
$this->assertCount(2, $this->response->getOpenEvents());
$this->assertCount(1, $this->response->getOpenEvents());
$this->assertEquals($start, $this->response->getOpenEvents()[0]->getBegin());
$this->assertEquals($end, $this->response->getOpenEvents()[0]->getEnd());
}
/**
* @test
* @throws \Source\Exceptions\EntityNotFoundException
*/
public function it_only_splits_on_locked_override(): void
{
......@@ -231,8 +233,9 @@ class StatusResponseTest extends TestCase
/**
* @test
* @throws \Source\Exceptions\EntityNotFoundException
*/
public function it_cuts_the_end(): void
public function overrides_cut_the_end(): void
{
$start = Carbon::now();
$this->rset->occurrences = [
......@@ -260,7 +263,7 @@ class StatusResponseTest extends TestCase
Carbon::now()->addSeconds(10)
));