feat: automatically purge applications with date ranges overlapping an accepted application

closes #869aug4uz
This commit is contained in:
Björn Fromme
2025-10-21 09:18:15 +02:00
parent 6c5115d5e7
commit f4b0dda38b
2 changed files with 412 additions and 0 deletions
@@ -0,0 +1,332 @@
<?php
declare(strict_types=1);
namespace App\Tests\EventListener;
use App\Entity\Application;
use App\Entity\Assignment;
use App\Entity\Destination;
use App\Entity\Disposition;
use App\Entity\Teamer;
use App\Entity\Upload;
use App\Event\DocumentConfirmedEvent;
use App\EventListener\InvalidateApplicationsListener;
use App\Repository\ApplicationRepository;
use Carbon\CarbonPeriod;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class InvalidateApplicationsListenerTest extends TestCase
{
private EntityManagerInterface&MockObject $entityManager;
private InvalidateApplicationsListener $listener;
private array $removedApplications = [];
protected function setUp(): void
{
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$logger = $this->createMock(LoggerInterface::class);
$this->removedApplications = [];
$this->entityManager
->method('remove')
->willReturnCallback(function (Application $application): void {
$this->removedApplications[] = $application;
});
$this->listener = new InvalidateApplicationsListener(
$this->entityManager,
$logger
);
}
public function testDoesNotInvalidateApplicationsForNonContractDocuments(): void
{
$document = $this->createMockUpload(Upload::TYPE_INVOICE);
$this->entityManager
->expects($this->never())
->method('getRepository');
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(0, $this->removedApplications);
}
public function testInvalidatesApplicationCompletelyWithinConfirmedPeriod(): void
{
$teamer = $this->createMock(Teamer::class);
$confirmedPeriod = new CarbonPeriod('2025-01-10', '2025-01-20');
$overlappingApplication = $this->createMockApplication('2025-01-12', '2025-01-18');
$document = $this->createMockContractWithPeriod($confirmedPeriod, $teamer);
$this->setupQueryMock($teamer, $confirmedPeriod, [$overlappingApplication]);
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(1, $this->removedApplications);
$this->assertSame($overlappingApplication, $this->removedApplications[0]);
}
public function testInvalidatesApplicationStartingBeforeAndOverlapping(): void
{
$teamer = $this->createMock(Teamer::class);
$confirmedPeriod = new CarbonPeriod('2025-01-10', '2025-01-20');
$overlappingApplication = $this->createMockApplication('2025-01-05', '2025-01-15');
$document = $this->createMockContractWithPeriod($confirmedPeriod, $teamer);
$this->setupQueryMock($teamer, $confirmedPeriod, [$overlappingApplication]);
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(1, $this->removedApplications);
}
public function testInvalidatesApplicationEndingAfterAndOverlapping(): void
{
$teamer = $this->createMock(Teamer::class);
$confirmedPeriod = new CarbonPeriod('2025-01-10', '2025-01-20');
$overlappingApplication = $this->createMockApplication('2025-01-15', '2025-01-25');
$document = $this->createMockContractWithPeriod($confirmedPeriod, $teamer);
$this->setupQueryMock($teamer, $confirmedPeriod, [$overlappingApplication]);
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(1, $this->removedApplications);
}
public function testInvalidatesApplicationSpanningEntireConfirmedPeriod(): void
{
$teamer = $this->createMock(Teamer::class);
$confirmedPeriod = new CarbonPeriod('2025-01-10', '2025-01-20');
$spanningApplication = $this->createMockApplication('2025-01-05', '2025-01-25');
$document = $this->createMockContractWithPeriod($confirmedPeriod, $teamer);
$this->setupQueryMock($teamer, $confirmedPeriod, [$spanningApplication]);
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(1, $this->removedApplications);
}
public function testInvalidatesApplicationWithExactSameDates(): void
{
$teamer = $this->createMock(Teamer::class);
$confirmedPeriod = new CarbonPeriod('2025-01-10', '2025-01-20');
$sameApplication = $this->createMockApplication('2025-01-10', '2025-01-20');
$document = $this->createMockContractWithPeriod($confirmedPeriod, $teamer);
$this->setupQueryMock($teamer, $confirmedPeriod, [$sameApplication]);
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(1, $this->removedApplications);
}
public function testDoesNotInvalidateApplicationEndingBeforeConfirmedPeriod(): void
{
$teamer = $this->createMock(Teamer::class);
$confirmedPeriod = new CarbonPeriod('2025-01-10', '2025-01-20');
$document = $this->createMockContractWithPeriod($confirmedPeriod, $teamer);
$this->setupQueryMock($teamer, $confirmedPeriod, []);
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(0, $this->removedApplications);
}
public function testDoesNotInvalidateApplicationStartingAfterConfirmedPeriod(): void
{
$teamer = $this->createMock(Teamer::class);
$confirmedPeriod = new CarbonPeriod('2025-01-10', '2025-01-20');
$document = $this->createMockContractWithPeriod($confirmedPeriod, $teamer);
$this->setupQueryMock($teamer, $confirmedPeriod, []);
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(0, $this->removedApplications);
}
public function testInvalidatesMultipleOverlappingApplications(): void
{
$teamer = $this->createMock(Teamer::class);
$confirmedPeriod = new CarbonPeriod('2025-01-10', '2025-01-20');
$application1 = $this->createMockApplication('2025-01-12', '2025-01-14');
$application2 = $this->createMockApplication('2025-01-15', '2025-01-18');
$application3 = $this->createMockApplication('2025-01-08', '2025-01-25');
$document = $this->createMockContractWithPeriod($confirmedPeriod, $teamer);
$this->setupQueryMock($teamer, $confirmedPeriod, [$application1, $application2, $application3]);
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(3, $this->removedApplications);
}
public function testDoesNotInvalidateApplicationTouchingOnBoundary(): void
{
$teamer = $this->createMock(Teamer::class);
$confirmedPeriod = new CarbonPeriod('2025-01-10', '2025-01-20');
$document = $this->createMockContractWithPeriod($confirmedPeriod, $teamer);
$this->setupQueryMock($teamer, $confirmedPeriod, []);
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(0, $this->removedApplications);
}
public function testDoesNotInvalidateRejectedApplications(): void
{
$teamer = $this->createMock(Teamer::class);
$confirmedPeriod = new CarbonPeriod('2025-01-10', '2025-01-20');
$document = $this->createMockContractWithPeriod($confirmedPeriod, $teamer);
$this->setupQueryMock($teamer, $confirmedPeriod, []);
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(0, $this->removedApplications);
}
public function testDoesNotInvalidateApplicationEndingOnSameDayAsConfirmedStart(): void
{
$teamer = $this->createMock(Teamer::class);
$confirmedPeriod = new CarbonPeriod('2025-01-10', '2025-01-20');
$document = $this->createMockContractWithPeriod($confirmedPeriod, $teamer);
$this->setupQueryMock($teamer, $confirmedPeriod, []);
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(0, $this->removedApplications);
}
public function testDoesNotInvalidateApplicationStartingOnSameDayAsConfirmedEnd(): void
{
$teamer = $this->createMock(Teamer::class);
$confirmedPeriod = new CarbonPeriod('2025-01-10', '2025-01-20');
$document = $this->createMockContractWithPeriod($confirmedPeriod, $teamer);
$this->setupQueryMock($teamer, $confirmedPeriod, []);
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(0, $this->removedApplications);
}
public function testInvalidatesApplicationWithOneDayActualOverlap(): void
{
$teamer = $this->createMock(Teamer::class);
$confirmedPeriod = new CarbonPeriod('2025-01-10', '2025-01-20');
$overlappingApplication = $this->createMockApplication('2025-01-05', '2025-01-11');
$document = $this->createMockContractWithPeriod($confirmedPeriod, $teamer);
$this->setupQueryMock($teamer, $confirmedPeriod, [$overlappingApplication]);
$event = new DocumentConfirmedEvent($document);
$this->listener->onDocumentConfirmed($event);
$this->assertCount(1, $this->removedApplications);
}
private function createMockUpload(string $type): Upload&MockObject
{
$upload = $this->createMock(Upload::class);
$upload->method('getType')->willReturn($type);
return $upload;
}
private function createMockContractWithPeriod(CarbonPeriod $period, Teamer $teamer): Upload&MockObject
{
$destination = $this->createMock(Destination::class);
$destination->method('getHotelCode')->willReturn('TST');
$destination->method('getDateFrom')->willReturn($period->start->toDateTimeImmutable());
$destination->method('getDateTo')->willReturn($period->end->toDateTimeImmutable());
$assignment = $this->createMock(Assignment::class);
$assignment->method('getEffectivePeriod')->willReturn($period);
$assignment->method('getDestination')->willReturn($destination);
$disposition = $this->createMock(Disposition::class);
$disposition->method('getAssignment')->willReturn($assignment);
$disposition->method('getTeamer')->willReturn($teamer);
$upload = $this->createMock(Upload::class);
$upload->method('getType')->willReturn(Upload::TYPE_CONTRACT);
$upload->method('getDisposition')->willReturn($disposition);
return $upload;
}
private function createMockApplication(string $dateFrom, string $dateTo): Application&MockObject
{
$destination = $this->createMock(Destination::class);
$destination->method('getHotelCode')->willReturn('TST');
$destination->method('getDateFrom')->willReturn(new \DateTimeImmutable($dateFrom));
$destination->method('getDateTo')->willReturn(new \DateTimeImmutable($dateTo));
$assignment = $this->createMock(Assignment::class);
$assignment->method('getDestination')->willReturn($destination);
$application = $this->createMock(Application::class);
$application->method('getAssignment')->willReturn($assignment);
return $application;
}
private function setupQueryMock(Teamer $teamer, CarbonPeriod $period, array $applications): void
{
$query = $this->createMock(AbstractQuery::class);
$query->method('getResult')->willReturn($applications);
$expr = $this->createMock(Expr::class);
$expr->method('andX')->willReturnSelf();
$expr->method('eq')->willReturnSelf();
$expr->method('lte')->willReturnSelf();
$expr->method('gte')->willReturnSelf();
$expr->method('neq')->willReturnSelf();
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->method('expr')->willReturn($expr);
$queryBuilder->method('innerJoin')->willReturnSelf();
$queryBuilder->method('where')->willReturnSelf();
$queryBuilder->method('setParameter')->willReturnSelf();
$queryBuilder->method('getQuery')->willReturn($query);
$repository = $this->createMock(ApplicationRepository::class);
$repository
->method('createQueryBuilder')
->with('application')
->willReturn($queryBuilder);
$this->entityManager
->method('getRepository')
->with(Application::class)
->willReturn($repository);
}
}