fix: adjust detection of overlapping applications
addresses #869ar02xk
This commit is contained in:
@@ -2,18 +2,22 @@
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Application;
|
||||
use App\Entity\Upload;
|
||||
use App\Event\ApplicationDeletedEvent;
|
||||
use App\Event\DocumentConfirmedEvent;
|
||||
use App\Repository\ApplicationRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
#[AsEventListener(event: DocumentConfirmedEvent::NAME, method: 'onDocumentConfirmed')]
|
||||
class InvalidateApplicationsListener
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ApplicationRepository $applicationRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
@@ -33,73 +37,39 @@ class InvalidateApplicationsListener
|
||||
return;
|
||||
}
|
||||
|
||||
$assignment = $disposition->getAssignment();
|
||||
$period = $assignment->getEffectivePeriod();
|
||||
if (null === $period = $disposition->getAssignment()->getEffectivePeriod()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find other applications by this teamer with overlapping date ranges
|
||||
// Two periods overlap when: (start1 < end2) AND (end1 > start2)
|
||||
// This catches all overlap scenarios while excluding boundary-only touching:
|
||||
// - Partial overlaps from either direction
|
||||
// - Complete containment in either direction
|
||||
// - Exact date matches
|
||||
// Note: Periods that only touch at boundaries (e.g., Jan 1-7 and Jan 7-12) are NOT considered overlapping
|
||||
// The effective dates are determined by checking assignment dates first, falling back to destination dates if null
|
||||
$qb = $this
|
||||
->entityManager
|
||||
->getRepository(Application::class)
|
||||
->createQueryBuilder('application');
|
||||
|
||||
/** @var array<Application> $applications */
|
||||
$applications = $qb
|
||||
->innerJoin('application.assignment', 'assignment')
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->eq('application.teamer', ':teamer'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateFrom'),
|
||||
$qb->expr()->lt('destination.dateFrom', ':dateTo')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateFrom'),
|
||||
$qb->expr()->lt('assignment.dateFrom', ':dateTo')
|
||||
)
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateTo'),
|
||||
$qb->expr()->gt('destination.dateTo', ':dateFrom')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
$qb->expr()->gt('assignment.dateTo', ':dateFrom')
|
||||
)
|
||||
),
|
||||
$qb->expr()->neq('application.status', ':status')
|
||||
))
|
||||
->setParameter('teamer', $teamer)
|
||||
->setParameter('dateFrom', $period->start->toDateTimeImmutable())
|
||||
->setParameter('dateTo', $period->end->toDateTimeImmutable())
|
||||
->setParameter('status', Application::STATUS_REJECTED)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
$applications = $this->applicationRepository->findOverlappingForTeamer($teamer, $period);
|
||||
|
||||
if (0 === count($applications)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($applications as $application) {
|
||||
$destination = $application->getAssignment()->getDestination();
|
||||
$assignment = $application->getAssignment();
|
||||
$this->logger->info('Deleted overlapping application', [
|
||||
'teamer' => $teamer,
|
||||
'destination' => $destination->getHotelCode(),
|
||||
'date_from' => $destination->getDateFrom()->format('Y-m-d'),
|
||||
'date_to' => $destination->getDateTo()->format('Y-m-d'),
|
||||
'application' => $application->getUuid(),
|
||||
'assignment' => $assignment->getUuid(),
|
||||
'teamer' => $teamer->getUuid(),
|
||||
'teamer_name' => (string) $teamer,
|
||||
'destination' => $assignment->getDestination()->getHotelCode(),
|
||||
'date_from' => $assignment->getEffectiveDateFrom()->format('Y-m-d'),
|
||||
'date_to' => $assignment->getEffectiveDateTo()->format('Y-m-d'),
|
||||
]);
|
||||
$this->entityManager->remove($application);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
// the staffing status of the affected assignments and the audit log are both updated by
|
||||
// listeners of this event, so it has to be dispatched after the applications are gone
|
||||
foreach ($applications as $application) {
|
||||
$this->eventDispatcher->dispatch(
|
||||
new ApplicationDeletedEvent($application),
|
||||
ApplicationDeletedEvent::NAME
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Entity\Teamer;
|
||||
use App\Model\ApplicationFilterDto;
|
||||
use App\Model\AssignmentFilterDto;
|
||||
use App\Repository\Traits\QueryHelperTrait;
|
||||
use Carbon\CarbonPeriod;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
@@ -161,6 +162,67 @@ class ApplicationRepository extends ServiceEntityRepository
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all applications of a teamer whose effective assignment period overlaps the given period.
|
||||
*
|
||||
* The effective period of an assignment is its own date range, falling back to the date range of
|
||||
* its destination whenever the assignment does not override it - the query mirrors
|
||||
* {@see Assignment::getEffectivePeriod()}. Boundaries count as an overlap: a teamer cannot end one
|
||||
* assignment on the same day another one begins.
|
||||
*
|
||||
* @return array<Application>
|
||||
*/
|
||||
public function findOverlappingForTeamer(Teamer $teamer, CarbonPeriod $period): array
|
||||
{
|
||||
return $this
|
||||
->getOverlappingForTeamerQuery($teamer, $period)
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
public function getOverlappingForTeamerQuery(Teamer $teamer, CarbonPeriod $period): Query
|
||||
{
|
||||
$qb = $this->createQueryBuilder('application');
|
||||
|
||||
return $qb
|
||||
->innerJoin('application.assignment', 'assignment')
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->eq('application.teamer', ':teamer'),
|
||||
$qb->expr()->neq('application.status', ':application_rejected'),
|
||||
$qb->expr()->neq('assignment.status', ':assignment_called_off'),
|
||||
$qb->expr()->isNull('assignment.deletedAt'),
|
||||
$qb->expr()->isNull('destination.deletedAt'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateFrom'),
|
||||
$qb->expr()->lte('destination.dateFrom', ':dateTo')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateFrom'),
|
||||
$qb->expr()->lte('assignment.dateFrom', ':dateTo')
|
||||
)
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateTo'),
|
||||
$qb->expr()->gte('destination.dateTo', ':dateFrom')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
$qb->expr()->gte('assignment.dateTo', ':dateFrom')
|
||||
)
|
||||
)
|
||||
))
|
||||
->setParameter('teamer', $teamer)
|
||||
->setParameter('application_rejected', Application::STATUS_REJECTED)
|
||||
->setParameter('assignment_called_off', Assignment::STATUS_CALLED_OFF)
|
||||
->setParameter('dateFrom', $period->start->toDateTimeImmutable())
|
||||
->setParameter('dateTo', $period->end->toDateTimeImmutable())
|
||||
->getQuery()
|
||||
;
|
||||
}
|
||||
|
||||
public function getCurrentByAssignment(Assignment $assignment): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('application');
|
||||
|
||||
@@ -37,17 +37,21 @@ class ApplicationValidator extends ConstraintValidator
|
||||
return;
|
||||
}
|
||||
|
||||
$assignment = $application->getAssignment();
|
||||
$destination = $assignment->getDestination();
|
||||
if (null === $period = $application->getAssignment()->getEffectivePeriod()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$applicationDateFrom = $destination->getDateFrom();
|
||||
$applicationDateTo = $destination->getDateTo();
|
||||
$applicationDateFrom = $period->start->toDateTimeImmutable();
|
||||
$applicationDateTo = $period->end->toDateTimeImmutable();
|
||||
|
||||
$qb = $this
|
||||
->dispositionRepository
|
||||
->createQueryBuilder('disposition')
|
||||
;
|
||||
|
||||
// the same overlap test as App\Repository\ApplicationRepository::findOverlappingForTeamer(),
|
||||
// so an application that passes this validation is not purged later on by
|
||||
// App\EventListener\InvalidateApplicationsListener
|
||||
$conflictingCount = $qb
|
||||
->select($qb->expr()->count('disposition'))
|
||||
->innerJoin('disposition.assignment', 'assignment')
|
||||
@@ -60,12 +64,22 @@ class ApplicationValidator extends ConstraintValidator
|
||||
$qb->expr()->isNull('destination.deletedAt'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->gt('destination.dateTo', ':application_date_from'),
|
||||
$qb->expr()->lt('destination.dateTo', ':application_date_to'),
|
||||
$qb->expr()->isNull('assignment.dateFrom'),
|
||||
$qb->expr()->lte('destination.dateFrom', ':application_date_to')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->lt('destination.dateFrom', ':application_date_to'),
|
||||
$qb->expr()->gt('destination.dateFrom', ':application_date_from'),
|
||||
$qb->expr()->isNotNull('assignment.dateFrom'),
|
||||
$qb->expr()->lte('assignment.dateFrom', ':application_date_to')
|
||||
)
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateTo'),
|
||||
$qb->expr()->gte('destination.dateTo', ':application_date_from')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
$qb->expr()->gte('assignment.dateTo', ':application_date_from')
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user