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

current progress on this

parent fabf7698
Pipeline #13192 failed with stages
in 1 minute and 40 seconds
......@@ -158,7 +158,7 @@ class Override
}
/**
* @param \Carbon\Carbon $date
* @param \Carbon\Carbon|null $date
* @return bool
*/
public function isActiveForDate(?Carbon $date): bool
......@@ -191,4 +191,44 @@ class Override
{
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
/**
* @inheritDoc
*/
public function activeOverrideForDoorBetween(string $doorId, Carbon $begin, Carbon $end): ?Override
public function activeOverridesForDoorBetween(string $doorId, Carbon $begin, Carbon $end): array
{
$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
from overrides as A
WHERE A.door_id = :DOOR_ID
AND ((:BEGIN, :END) OVERLAPS (A.start, A.end))
ORDER BY A.created_at DESC
LIMIT 1
ORDER BY A.start
QUERY;
$override = $this->db->selectOne($query, [
$overrides = $this->db->select($query, [
':DOOR_ID' => self::castToInt($doorId),
':BEGIN' => $begin,
':END' => $end,
]);
if (!$override) {
return null;
}
return $this->toOverrideFromRaw($override);
return array_map(static function (\App\Override $override) {
return self::toOverrideFromRaw($override);
}, $overrides);
}
/**
......
......@@ -32,9 +32,9 @@ class InMemoryOverridesRepository implements OverridesRepository
if ($begin && $end) {
$include = $include &&
($begin->lessThanOrEqualTo($override->getEnd()) && $end->greaterThanOrEqualTo($override->getStart()));
} elseif ($begin) {
} else if ($begin) {
$include = $include && $begin->lessThanOrEqualTo($override->getEnd());
} elseif ($end) {
} else if ($end) {
$include = $include && $end->greaterThanOrEqualTo($override->getStart());
}
......@@ -45,7 +45,7 @@ class InMemoryOverridesRepository implements OverridesRepository
/**
* @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) {
return $override->hasDoorIdOf($doorId) &&
......@@ -53,8 +53,9 @@ class InMemoryOverridesRepository implements OverridesRepository
}));
return $overrides
->sortBy(fn (Override $item) => $item->getCreatedAt(), SORT_DESC, true)
->first();
->sortBy(fn (Override $item) => $item->getStart())
->values()
->all();
}
/**
......
......@@ -20,14 +20,15 @@ interface OverridesRepository
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 \Carbon\Carbon $begin
* @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
......
......@@ -30,22 +30,29 @@ class OverrideAuthorizer implements AccessAuthorizer
public function check(?User $user, Carbon $date, Door $door, string $doorcode, ?string $commandString): int
{
// 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) {
return self::CONTINUE;
$open = false;
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()) {
case Override::TYPE_LOCKED:
$this->reason = 'Door is in override locked mode.';
return self::DENY;
case Override::TYPE_OPEN:
$this->reason = 'Door is in override open mode.';
return self::ALLOW;
default:
return self::CONTINUE;
if ($open) {
$this->reason = 'Door is in override open mode.';
return self::ALLOW;
}
return self::CONTINUE;
}
/**
......
......@@ -95,9 +95,9 @@ class OverrideCommand extends Command
return false;
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 {
$this->overrides->updateOverride($override->getId(), new Override(
$override->getId(),
......@@ -109,7 +109,7 @@ class OverrideCommand extends Command
Carbon::now()->subSecond()
));
} catch (EntityNotFoundException $e) {
return false;
// Do nothing and continue on
}
}
......
......@@ -10,62 +10,173 @@ use Source\Entities\ScheduleEvent;
trait ScheduleEventOverrideMerge
{
/**
* @param \Carbon\Carbon $start
* @param \Carbon\Carbon $end
* @param \Source\Entities\Override|null $override
* @return \Source\Entities\ScheduleEvent[]
* @param \Source\Entities\Override[] $closed
* @param \Source\Entities\Override[] $open
* @return \Source\Entities\Override[]
*/
protected function mergeOverrideAndScheduleEvent(Carbon $start, Carbon $end, ?Override $override): array
protected function splitOpenOverrides(array $closed, array $open): 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
* Preprocessing:
* Clip all overrides around closed mode overrides, then process all overrides. once we have resolved all
* overlap.
* Closed modes have precedence over open overrides.
*/
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),
];
$nonOverlappingOverrides = [];
// TODO: Rework this there is bug when what happens if your open needs to be split by multiple closeds
// So you end up with a segmented thing where you will have a conclomeration of stuff, we need to merge
// with previous processed segments
foreach ($open as $oo) {
foreach ($closed as $co) {
// No action needed if no overlap
if ($co->hasNoOverlapWith($oo)) {
continue;
}
/*
* 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
return [new ScheduleEvent($start, $end)];
foreach ($closedOverrides as $override) {
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 [];
}
if ($overlapBegin) {
return [new ScheduleEvent($override->getEnd(), $end)];
// Next override as this one has no overlap.
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 [new ScheduleEvent($start, $end)];
return $splits;
}
}
......@@ -59,13 +59,15 @@ class ScheduleEvents implements ScheduleEventsUseCase
Presenter $presenter
): void {
/*
* Ignore override overlaps, allow override edits
* only take latest override
* Override is immediate for not planned things
*
* Only one override can be active at a time
* Need to merge overrides :(
* I tried imposing a only one override at a time, but... they're used like schedules are supposed to be
* anyway so I'm gonna just have to make them work like schedules...
*/
$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);
$userSchedules = [];
......@@ -77,8 +79,8 @@ class ScheduleEvents implements ScheduleEventsUseCase
$response = new ResponseModel();
if ($override && $override->hasTypeOf(Override::TYPE_OPEN)) {
$response->addOpenEvent(new ScheduleEvent($override->getStart(), $override->getEnd()));
foreach ($this->splitOpenOverrides($closed, $open) as $openOverride) {
$response->addOpenEvent(new ScheduleEvent($openOverride->getStart(), $openOverride->getEnd()));
}
// Get all open schedule times in the specified range.
......@@ -94,7 +96,7 @@ class ScheduleEvents implements ScheduleEventsUseCase
foreach ($this->rset->occurrencesBetween($b, $end) as $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.
if ($scheduleEvent->getBegin() < $begin) {
$scheduleEvent->setBegin($begin);
......
......@@ -130,9 +130,9 @@ class OverrideDatabaseTest extends DatabaseTestCase
Carbon::now()->addHour(),
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
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());
}
/**
......
Markdown is supported
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