Commit 47f9024c authored by Jacob Priddy's avatar Jacob Priddy 👌
Browse files

current progress on this

parent fabf7698
Pipeline #13192 failed with stages
in 1 minute and 40 seconds
...@@ -158,7 +158,7 @@ class Override ...@@ -158,7 +158,7 @@ class Override
} }
/** /**
* @param \Carbon\Carbon $date * @param \Carbon\Carbon|null $date
* @return bool * @return bool
*/ */
public function isActiveForDate(?Carbon $date): bool public function isActiveForDate(?Carbon $date): bool
...@@ -191,4 +191,44 @@ class Override ...@@ -191,4 +191,44 @@ class Override
{ {
return $this->getId() === (int)$id; return $this->getId() === (int)$id;
} }
/**
* Determines if the current override engulfs another
*
* @param \Source\Entities\Override $o
* @return bool
*/
public function engulfs(Override $o): bool
{
return $this->engulfsDates($o->getStart(), $o->getEnd());
}
/**
* @param \Carbon\Carbon $start
* @param \Carbon\Carbon $end
* @return bool
*/
public function engulfsDates(Carbon $start, Carbon $end): bool
{
return $this->getStart() < $start && $this->getEnd() > $end;
}
/**
* @param \Source\Entities\Override $o
* @return bool
*/
public function hasNoOverlapWith(Override $o): bool
{
return $this->hasNoOverlapWithDates($o->getStart(), $o->getEnd());
}
/**
* @param \Carbon\Carbon $start
* @param \Carbon\Carbon $end
* @return bool
*/
public function hasNoOverlapWithDates(Carbon $start, Carbon $end): bool
{
return $this->getStart() > $end || $this->getEnd() < $start;
}
} }
...@@ -95,28 +95,25 @@ class DatabaseOverridesRepository implements OverridesRepository ...@@ -95,28 +95,25 @@ class DatabaseOverridesRepository implements OverridesRepository
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function activeOverrideForDoorBetween(string $doorId, Carbon $begin, Carbon $end): ?Override public function activeOverridesForDoorBetween(string $doorId, Carbon $begin, Carbon $end): array
{ {
$query = <<<QUERY $query = <<<QUERY
select A.id, A.reason, A.user_id, A.door_id, A.type, A.start, A.end, A.created_at, A.updated_at select A.id, A.reason, A.user_id, A.door_id, A.type, A.start, A.end, A.created_at, A.updated_at
from overrides as A from overrides as A
WHERE A.door_id = :DOOR_ID WHERE A.door_id = :DOOR_ID
AND ((:BEGIN, :END) OVERLAPS (A.start, A.end)) AND ((:BEGIN, :END) OVERLAPS (A.start, A.end))
ORDER BY A.created_at DESC ORDER BY A.start
LIMIT 1
QUERY; QUERY;
$override = $this->db->selectOne($query, [ $overrides = $this->db->select($query, [
':DOOR_ID' => self::castToInt($doorId), ':DOOR_ID' => self::castToInt($doorId),
':BEGIN' => $begin, ':BEGIN' => $begin,
':END' => $end, ':END' => $end,
]); ]);
if (!$override) { return array_map(static function (\App\Override $override) {
return null; return self::toOverrideFromRaw($override);
} }, $overrides);
return $this->toOverrideFromRaw($override);
} }
/** /**
......
...@@ -32,9 +32,9 @@ class InMemoryOverridesRepository implements OverridesRepository ...@@ -32,9 +32,9 @@ class InMemoryOverridesRepository implements OverridesRepository
if ($begin && $end) { if ($begin && $end) {
$include = $include && $include = $include &&
($begin->lessThanOrEqualTo($override->getEnd()) && $end->greaterThanOrEqualTo($override->getStart())); ($begin->lessThanOrEqualTo($override->getEnd()) && $end->greaterThanOrEqualTo($override->getStart()));
} elseif ($begin) { } else if ($begin) {
$include = $include && $begin->lessThanOrEqualTo($override->getEnd()); $include = $include && $begin->lessThanOrEqualTo($override->getEnd());
} elseif ($end) { } else if ($end) {
$include = $include && $end->greaterThanOrEqualTo($override->getStart()); $include = $include && $end->greaterThanOrEqualTo($override->getStart());
} }
...@@ -45,7 +45,7 @@ class InMemoryOverridesRepository implements OverridesRepository ...@@ -45,7 +45,7 @@ class InMemoryOverridesRepository implements OverridesRepository
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function activeOverrideForDoorBetween(string $doorId, Carbon $begin, Carbon $end): ?Override public function activeOverridesForDoorBetween(string $doorId, Carbon $begin, Carbon $end): array
{ {
$overrides = collect(array_filter($this->overrides, static function (Override $override) use ($doorId, $begin, $end) { $overrides = collect(array_filter($this->overrides, static function (Override $override) use ($doorId, $begin, $end) {
return $override->hasDoorIdOf($doorId) && return $override->hasDoorIdOf($doorId) &&
...@@ -53,8 +53,9 @@ class InMemoryOverridesRepository implements OverridesRepository ...@@ -53,8 +53,9 @@ class InMemoryOverridesRepository implements OverridesRepository
})); }));
return $overrides return $overrides
->sortBy(fn (Override $item) => $item->getCreatedAt(), SORT_DESC, true) ->sortBy(fn (Override $item) => $item->getStart())
->first(); ->values()
->all();
} }
/** /**
......
...@@ -20,14 +20,15 @@ interface OverridesRepository ...@@ -20,14 +20,15 @@ interface OverridesRepository
public function filterHistory(?string $doorId, ?string $userId, ?Carbon $begin, ?Carbon $end): array; public function filterHistory(?string $doorId, ?string $userId, ?Carbon $begin, ?Carbon $end): array;
/** /**
* Gets active override during date (if any). Returns null otherwise. * Gets active override during date (if any). Closed modes take precedence over open modes.
* They are sorted by their start date
* *
* @param string $doorId * @param string $doorId
* @param \Carbon\Carbon $begin * @param \Carbon\Carbon $begin
* @param \Carbon\Carbon $end * @param \Carbon\Carbon $end
* @return \Source\Entities\Override|null * @return Override[]
*/ */
public function activeOverrideForDoorBetween(string $doorId, Carbon $begin, Carbon $end): ?Override; public function activeOverridesForDoorBetween(string $doorId, Carbon $begin, Carbon $end): array;
/** /**
* Persists an override to the application. Only the latest override is ever looked at for scheduling * Persists an override to the application. Only the latest override is ever looked at for scheduling
......
...@@ -30,22 +30,29 @@ class OverrideAuthorizer implements AccessAuthorizer ...@@ -30,22 +30,29 @@ class OverrideAuthorizer implements AccessAuthorizer
public function check(?User $user, Carbon $date, Door $door, string $doorcode, ?string $commandString): int public function check(?User $user, Carbon $date, Door $door, string $doorcode, ?string $commandString): int
{ {
// Get currently active override if there is any // Get currently active override if there is any
$active = $this->overrides->activeOverrideForDoorBetween($door->getId(), $date, $date); $overrides = $this->overrides->activeOverridesForDoorBetween($door->getId(), $date, $date);
if (!$active) { $open = false;
return self::CONTINUE;
foreach ($overrides as $active) {
switch ($active->getType()) {
case Override::TYPE_LOCKED:
$this->reason = 'Door is in override locked mode.';
return self::DENY;
case Override::TYPE_OPEN:
$open = true;
break;
default:
break;
}
} }
switch ($active->getType()) { if ($open) {
case Override::TYPE_LOCKED: $this->reason = 'Door is in override open mode.';
$this->reason = 'Door is in override locked mode.'; return self::ALLOW;
return self::DENY;
case Override::TYPE_OPEN:
$this->reason = 'Door is in override open mode.';
return self::ALLOW;
default:
return self::CONTINUE;
} }
return self::CONTINUE;
} }
/** /**
......
...@@ -95,9 +95,9 @@ class OverrideCommand extends Command ...@@ -95,9 +95,9 @@ class OverrideCommand extends Command
return false; return false;
case self::CLEAR_MODE: case self::CLEAR_MODE:
$override = $this->overrides->activeOverrideForDoorBetween($door->getId(), Carbon::now(), Carbon::now()); $overrides = $this->overrides->activeOverridesForDoorBetween($door->getId(), Carbon::now(), Carbon::now());
if ($override) { foreach ($overrides as $override) {
try { try {
$this->overrides->updateOverride($override->getId(), new Override( $this->overrides->updateOverride($override->getId(), new Override(
$override->getId(), $override->getId(),
...@@ -109,7 +109,7 @@ class OverrideCommand extends Command ...@@ -109,7 +109,7 @@ class OverrideCommand extends Command
Carbon::now()->subSecond() Carbon::now()->subSecond()
)); ));
} catch (EntityNotFoundException $e) { } catch (EntityNotFoundException $e) {
return false; // Do nothing and continue on
} }
} }
......
...@@ -10,62 +10,173 @@ use Source\Entities\ScheduleEvent; ...@@ -10,62 +10,173 @@ use Source\Entities\ScheduleEvent;
trait ScheduleEventOverrideMerge trait ScheduleEventOverrideMerge
{ {
/** /**
* @param \Carbon\Carbon $start * @param \Source\Entities\Override[] $closed
* @param \Carbon\Carbon $end * @param \Source\Entities\Override[] $open
* @param \Source\Entities\Override|null $override * @return \Source\Entities\Override[]
* @return \Source\Entities\ScheduleEvent[]
*/ */
protected function mergeOverrideAndScheduleEvent(Carbon $start, Carbon $end, ?Override $override): array protected function splitOpenOverrides(array $closed, array $open): array
{ {
/* /*
* Parsing of schedules * Preprocessing:
* * Clip all overrides around closed mode overrides, then process all overrides. once we have resolved all
* schedule no overlapping override * overlap.
* - schedule encloses override * Closed modes have precedence over open overrides.
* - 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)) { $nonOverlappingOverrides = [];
// If the beginning of the schedule overlaps with the override
$overlapBegin = $override->isActiveForDate($start); // TODO: Rework this there is bug when what happens if your open needs to be split by multiple closeds
// If the end of the schedule overlaps with the override // So you end up with a segmented thing where you will have a conclomeration of stuff, we need to merge
$overlapEnd = $override->isActiveForDate($end); // with previous processed segments
if (!$overlapBegin && !$overlapEnd) { foreach ($open as $oo) {
if ($start->isBefore($override->getStart()) && $end->isAfter($override->getEnd())) { foreach ($closed as $co) {
// Override is enclosed by the event, break up the open event // No action needed if no overlap
return [ if ($co->hasNoOverlapWith($oo)) {
new ScheduleEvent($start, $override->getStart()), continue;
new ScheduleEvent($override->getEnd(), $end), }
];
/*
* Closed engulfs an open
* |----open----|
* |-----closed--------|
* In this case ignore the open override.
*/
if ($co->engulfs($oo)) {
continue;
}
/*
* Open is at the left side of a closed
* |---open---|
* |-----closed----|
* In this case cut the open override.
*
* Node: This case and the other edge case take care of when an open engulfs a closed
*/
if ($oo->getStart() < $co->getStart()) {
$nonOverlappingOverrides[] = new Override(
$oo->getId(),
$oo->getReason(),
$oo->getUserId(),
$oo->getDoorId(),
$oo->getType(),
$oo->getStart(),
$co->getStart()
);
}
/*
* Open is at the right side of a closed
* |---open---|
* |-----closed----|
* In this case cut the open override.
*
* Node: This case and the other edge case take care of when an open engulfs a closed
*/
if ($oo->getEnd() > $co->getEnd()) {
$nonOverlappingOverrides[] = new Override(
$oo->getId(),
$oo->getReason(),
$oo->getUserId(),
$oo->getDoorId(),
$oo->getType(),
$co->getEnd(),
$oo->getEnd()
);
} }
}
}
return $nonOverlappingOverrides;
}
/**
* @param \Source\Entities\Override[] $closedOverrides
* @return \Source\Entities\Override[]
*/
protected function mergeClosedOverrides(array $closedOverrides): array
{
if (count($closedOverrides) < 1) {
return [];
}
$merged = [];
$prev = array_shift($closedOverrides);
// override does not overlap with event add whole event range foreach ($closedOverrides as $override) {
return [new ScheduleEvent($start, $end)]; if ($override->getStart() < $prev->getEnd()) {
$prev = new Override(
$prev->getId(),
$prev->getReason(),
$prev->getUserId(),
$prev->getDoorId(),
$prev->getType(),
$prev->getStart(),
$override->getEnd()
);
} else {
$merged[] = $prev;
$prev = $override;
} }
}
return $merged;
}
if ($overlapBegin && $overlapEnd) { /**
// Override envelops the event, ignore the event * This method breaks up a time range defined from a non overlapping list of closed overrides.
* This relies on there being NO OVERLAP between closed overrides.
*
* @param \Carbon\Carbon $start
* @param \Carbon\Carbon $end
* @param \Source\Entities\Override[] $closedOverrides
* @return \Source\Entities\ScheduleEvent[]
*/
protected function mergeOverridesAndScheduleEvent(Carbon $start, Carbon $end, array $closedOverrides): array
{
$splits = [];
foreach ($closedOverrides as $override) {
/*
* Closed engulfs the open segment
* |----open----|
* |-----closed--------|
* In this case ignore the schedule segment
*/
if ($override->engulfsDates($start, $end)) {
return []; return [];
} }
if ($overlapBegin) { // Next override as this one has no overlap.
return [new ScheduleEvent($override->getEnd(), $end)]; if ($override->hasNoOverlapWithDates($start, $end)) {
continue;
} }
return [new ScheduleEvent($start, $override->getStart())]; /*
* Open is at the left side of a closed
* |---open---|
* |-----closed----|
* In this case cut the open schedule segment.
*
* Node: This case and the other edge case take care of when an open engulfs a closed
*/
if ($start < $override->getStart()) {
$splits[] = new ScheduleEvent($start, $override->getStart());
}
/*
* Open is at the right side of a closed
* |---open---|
* |-----closed----|
* In this case cut the open override.
*
* Node: This case and the other edge case take care of when an open engulfs a closed
*/
if ($end > $override->getEnd()) {
$splits[] = new ScheduleEvent($override->getEnd(), $end);
}
} }
// No override to worry about, add the event return $splits;
return [new ScheduleEvent($start, $end)];
} }
} }
...@@ -59,13 +59,15 @@ class ScheduleEvents implements ScheduleEventsUseCase ...@@ -59,13 +59,15 @@ class ScheduleEvents implements ScheduleEventsUseCase
Presenter $presenter Presenter $presenter
): void { ): void {
/* /*
* Ignore override overlaps, allow override edits * Need to merge overrides :(
* only take latest override * I tried imposing a only one override at a time, but... they're used like schedules are supposed to be
* Override is immediate for not planned things * anyway so I'm gonna just have to make them work like schedules...
*
* Only one override can be active at a time
*/ */
$override = $this->overrides->activeOverrideForDoorBetween($doorId, $begin, $end); $overrides = $this->overrides->activeOverridesForDoorBetween($doorId, $begin, $end);
$closed = $this->mergeClosedOverrides(
array_filter($overrides, static fn (Override $o) => $o->hasTypeOf(Override::TYPE_LOCKED))
);
$open = array_filter($overrides, static fn (Override $o) => $o->hasTypeOf(Override::TYPE_OPEN));
$openSchedules = $this->doorSchedules->getSchedulesForDoorBetween($doorId, $begin, $end, Schedule::TYPE_OPEN_MODE); $openSchedules = $this->doorSchedules->getSchedulesForDoorBetween($doorId, $begin, $end, Schedule::TYPE_OPEN_MODE);
$userSchedules = []; $userSchedules = [];
...@@ -77,8 +79,8 @@ class ScheduleEvents implements ScheduleEventsUseCase ...@@ -77,8 +79,8 @@ class ScheduleEvents implements ScheduleEventsUseCase
$response = new ResponseModel(); $response = new ResponseModel();
if ($override && $override->hasTypeOf(Override::TYPE_OPEN)) { foreach ($this->splitOpenOverrides($closed, $open) as $openOverride) {
$response->addOpenEvent(new ScheduleEvent($override->getStart(), $override->getEnd())); $response->addOpenEvent(new ScheduleEvent($openOverride->getStart(), $openOverride->getEnd()));
} }
// Get all open schedule times in the specified range. // Get all open schedule times in the specified range.
...@@ -94,7 +96,7 @@ class ScheduleEvents implements ScheduleEventsUseCase ...@@ -94,7 +96,7 @@ class ScheduleEvents implements ScheduleEventsUseCase
foreach ($this->rset->occurrencesBetween($b, $end) as $eventStart) { foreach ($this->rset->occurrencesBetween($b, $end) as $eventStart) {
$eventEnd = $schedule->addDuration($eventStart); $eventEnd = $schedule->addDuration($eventStart);
foreach ($this->mergeOverrideAndScheduleEvent($eventStart, $eventEnd, $override) as $scheduleEvent) { foreach ($this->mergeOverridesAndScheduleEvent($eventStart, $eventEnd, $closed) as $scheduleEvent) {
// Lock to be beginning and end date times requested. // Lock to be beginning and end date times requested.
if ($scheduleEvent->getBegin() < $begin) { if ($scheduleEvent->getBegin() < $begin) {
$scheduleEvent->setBegin($begin); $scheduleEvent->setBegin($begin);
......
...@@ -130,9 +130,9 @@ class OverrideDatabaseTest extends DatabaseTestCase ...@@ -130,9 +130,9 @@ class OverrideDatabaseTest extends DatabaseTestCase
Carbon::now()->addHour(), Carbon::now()->addHour(),
Carbon::now()->subMinute() Carbon::now()->subMinute()
)); ));
self::assertNull($this->repository->activeOverrideForDoorBetween($d1->getId(), Carbon::now(), Carbon::now()->addSecond())); self::assertNull($this->repository->activeOverridesForDoorBetween($d1->getId(), Carbon::now(), Carbon::now()->addSecond()));
// It picks the latest override // It picks the latest override
self::assertEquals(Override::TYPE_OPEN, $this->repository->activeOverrideForDoorBetween($d1->getId(), Carbon::now(), Carbon::now()->addHours(3))->getType()); self::assertEquals(Override::TYPE_OPEN, $this->repository->activeOverridesForDoorBetween($d1->getId(), Carbon::now(), Carbon::now()->addHours(3))->getType());
} }
/** /**
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment