createOverlappingQuery()->getDQL(); // the assignment period is the date range of its destination $this->assertStringContainsString('destination.dateFrom <= :dateTo', $dql); $this->assertStringContainsString('destination.dateTo >= :dateFrom', $dql); // strict comparisons would let assignments that touch on a boundary pass and could never // match a single day assignment at all $this->assertDoesNotMatchRegularExpression('/date(From|To) [<>] :date(From|To)/', $dql); } public function testOverlappingQueryExcludesRejectedApplicationsAndDeadAssignments(): void { $dql = $this->createOverlappingQuery()->getDQL(); $this->assertStringContainsString('application.teamer = :teamer', $dql); $this->assertStringContainsString('application.status <> :application_rejected', $dql); $this->assertStringContainsString('assignment.status <> :assignment_called_off', $dql); $this->assertStringContainsString('assignment.deletedAt IS NULL', $dql); $this->assertStringContainsString('destination.deletedAt IS NULL', $dql); } public function testOverlappingQueryBindsThePeriodBoundaries(): void { $teamer = new Teamer(); $period = new CarbonPeriodImmutable('2025-01-10', '2025-01-20'); $query = $this->createOverlappingQuery($teamer, $period); $this->assertSame($teamer, $query->getParameter('teamer')->getValue()); $this->assertSame(Application::STATUS_REJECTED, $query->getParameter('application_rejected')->getValue()); $this->assertSame(Assignment::STATUS_CALLED_OFF, $query->getParameter('assignment_called_off')->getValue()); $this->assertSame('2025-01-10', $query->getParameter('dateFrom')->getValue()->format('Y-m-d')); $this->assertSame('2025-01-20', $query->getParameter('dateTo')->getValue()->format('Y-m-d')); } private function createOverlappingQuery(?Teamer $teamer = null, ?CarbonPeriodImmutable $period = null): Query { /** @var EntityManagerInterface $entityManager */ $entityManager = static::getContainer()->get(EntityManagerInterface::class); /** @var ApplicationRepository $repository */ $repository = $entityManager->getRepository(Application::class); return $repository->getOverlappingForTeamerQuery( $teamer ?? new Teamer(), $period ?? new CarbonPeriodImmutable('2025-01-10', '2025-01-20') ); } public function testApplicationsByDestinationGroupsByTheNormalizedHotelCode(): void { $dql = $this->createApplicationsByDestinationQuery()->getDQL(); // the SER prefix is stripped so a house counts once, however its code is spelled $this->assertStringContainsString('SUBSTRING(destination.hotelCode, 4, 3)', $dql); $this->assertStringContainsString('SUBSTRING(destination.hotelCode, 1, 3)', $dql); $this->assertStringContainsString('GROUP BY hotelCode', $dql); // a destination lives on the assignment, not the application $this->assertStringContainsString('INNER JOIN application.assignment assignment', $dql); $this->assertStringContainsString('INNER JOIN assignment.destination destination', $dql); } public function testApplicationsByDestinationCountsEveryApplicationRegardlessOfStatus(): void { $query = $this->createApplicationsByDestinationQuery(); $dql = $query->getDQL(); // status moves over an application's lifetime, so it is not broken out - a plain count $this->assertStringContainsString('COUNT(application.id)', $dql); $this->assertStringNotContainsStringIgnoringCase('application.status', $dql); $this->assertNull($query->getParameter('statusNew')); } public function testApplicationsByDestinationAppliesTheSeasonBoundsOnlyWhenGiven(): void { $unfiltered = $this->createApplicationsByDestinationQuery(); $this->assertNull($unfiltered->getParameter('dateFrom')); $this->assertNull($unfiltered->getParameter('dateTo')); $filtered = $this->createApplicationsByDestinationQuery( new \DateTimeImmutable('2025-05-01'), new \DateTimeImmutable('2025-09-30'), ); $dql = $filtered->getDQL(); // the season is the destination's date range, same as every other statistics screen $this->assertStringContainsString('destination.dateFrom >= :dateFrom', $dql); $this->assertStringContainsString('destination.dateTo <= :dateTo', $dql); $this->assertSame('2025-05-01', $filtered->getParameter('dateFrom')->getValue()->format('Y-m-d')); $this->assertSame('2025-09-30', $filtered->getParameter('dateTo')->getValue()->format('Y-m-d')); } private function createApplicationsByDestinationQuery( ?\DateTimeImmutable $dateFrom = null, ?\DateTimeImmutable $dateTo = null, ): Query { /** @var EntityManagerInterface $entityManager */ $entityManager = static::getContainer()->get(EntityManagerInterface::class); /** @var ApplicationRepository $repository */ $repository = $entityManager->getRepository(Application::class); return $repository->getApplicationsByDestinationQuery($dateFrom, $dateTo); } }