diff --git a/docs/statistics.md b/docs/statistics.md index 45c3bb5..1eb897f 100644 --- a/docs/statistics.md +++ b/docs/statistics.md @@ -204,9 +204,8 @@ pinned in tests without a database. ### A period means the season, not when the row was written -`StatisticsDateBasis::SEASON` (the default) filters on the assignment's dates, falling back to -the destination's — the translation of `Assignment::getEffectivePeriod()` into DQL, which -lives in `src/Repository/Filter/SeasonPeriodFilter.php` and is shared with +`StatisticsDateBasis::SEASON` (the default) filters on the destination's date range through +`src/Repository/Filter/SeasonPeriodFilter.php`, which is shared with `DispositionRepository`. This matches every other statistics screen and `StatisticsFilterType`. Change what a season means there, once, not per screen. diff --git a/migrations/Version20260831144511.php b/migrations/Version20260831144511.php new file mode 100644 index 0000000..de85239 --- /dev/null +++ b/migrations/Version20260831144511.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE assignment DROP date_from, DROP date_to'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE assignment ADD date_from DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\', ADD date_to DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\''); + } +} diff --git a/src/Controller/HouseManager/Feedback/IndexController.php b/src/Controller/HouseManager/Feedback/IndexController.php index c4eee2f..f2d2e75 100644 --- a/src/Controller/HouseManager/Feedback/IndexController.php +++ b/src/Controller/HouseManager/Feedback/IndexController.php @@ -36,7 +36,7 @@ class IndexController extends AbstractController $request->query->getInt('page', 1), $request->query->getInt('limit', 50), [ - 'defaultSortFieldName' => 'assignment.dateFrom', + 'defaultSortFieldName' => 'destination.dateFrom', 'defaultSortDirection' => 'asc', ] ); diff --git a/src/Controller/Teamer/Application/IndexController.php b/src/Controller/Teamer/Application/IndexController.php index 1d26904..264c386 100644 --- a/src/Controller/Teamer/Application/IndexController.php +++ b/src/Controller/Teamer/Application/IndexController.php @@ -37,7 +37,7 @@ class IndexController extends AbstractController $request->query->getInt('page', 1), 9, [ - 'defaultSortFieldName' => 'assignment.dateFrom', + 'defaultSortFieldName' => 'destination.dateFrom', 'defaultSortDirection' => 'asc', ] ); diff --git a/src/Controller/Teamer/Bookmark/IndexController.php b/src/Controller/Teamer/Bookmark/IndexController.php index f155a6d..07206ba 100644 --- a/src/Controller/Teamer/Bookmark/IndexController.php +++ b/src/Controller/Teamer/Bookmark/IndexController.php @@ -37,7 +37,7 @@ class IndexController extends AbstractController $request->query->getInt('page', 1), 9, [ - 'defaultSortFieldName' => 'assignment.dateFrom', + 'defaultSortFieldName' => 'destination.dateFrom', 'defaultSortDirection' => 'asc', ] ); diff --git a/src/Controller/Teamer/Disposition/IcsController.php b/src/Controller/Teamer/Disposition/IcsController.php index 5371237..299653c 100644 --- a/src/Controller/Teamer/Disposition/IcsController.php +++ b/src/Controller/Teamer/Disposition/IcsController.php @@ -35,8 +35,8 @@ class IcsController extends AbstractController $filename = sprintf( 'Einteilung_%s_%s-%s.ics', $jobProfile->getName(), - $assignment->getEffectivePeriod()->start->format('d.m.Y'), - $assignment->getEffectivePeriod()->end->format('d.m.Y') + $destination->getDateFrom()->format('d.m.Y'), + $destination->getDateTo()->format('d.m.Y') ); $slugger = new AsciiSlugger(); $filename = $slugger->slug($filename); diff --git a/src/Controller/Teamer/Disposition/RecentController.php b/src/Controller/Teamer/Disposition/RecentController.php index 8afa274..533737c 100644 --- a/src/Controller/Teamer/Disposition/RecentController.php +++ b/src/Controller/Teamer/Disposition/RecentController.php @@ -37,7 +37,7 @@ class RecentController extends AbstractController $request->query->getInt('page', 1), 9, [ - 'defaultSortFieldName' => 'assignment.dateFrom', + 'defaultSortFieldName' => 'destination.dateFrom', 'defaultSortDirection' => 'asc', ] ); diff --git a/src/Controller/Teamer/Disposition/UpcomingController.php b/src/Controller/Teamer/Disposition/UpcomingController.php index 47b126f..9e7d873 100644 --- a/src/Controller/Teamer/Disposition/UpcomingController.php +++ b/src/Controller/Teamer/Disposition/UpcomingController.php @@ -37,7 +37,7 @@ class UpcomingController extends AbstractController $request->query->getInt('page', 1), 9, [ - 'defaultSortFieldName' => 'assignment.dateFrom', + 'defaultSortFieldName' => 'destination.dateFrom', 'defaultSortDirection' => 'asc', ] ); diff --git a/src/Entity/Assignment.php b/src/Entity/Assignment.php index 1872b84..f5e7325 100644 --- a/src/Entity/Assignment.php +++ b/src/Entity/Assignment.php @@ -6,7 +6,6 @@ use App\Entity\Traits\BlameableEntity; use App\Entity\Traits\SoftDeletableEntity; use App\Entity\Traits\TimestampableEntity; use App\Repository\AssignmentRepository; -use Carbon\CarbonPeriodImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; @@ -64,13 +63,6 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa #[ORM\Column] private bool $skipFormalities = false; - #[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)] - private ?\DateTimeImmutable $dateFrom = null; - - #[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)] - #[Assert\GreaterThan(propertyPath: 'dateFrom', message: 'Das Enddatum muss nach dem Beginndatum liegen')] - private ?\DateTimeImmutable $dateTo = null; - #[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)] private ?\DateTimeImmutable $pickupDate = null; @@ -133,8 +125,6 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa ->setAvailableDispositions($assignment->getAvailableDispositions()) ->setShowAvailableDispositions($assignment->isShowAvailableDispositions()) ->setSkipFormalities($assignment->isSkipFormalities()) - ->setDateFrom($assignment->getDateFrom()) - ->setDateTo($assignment->getDateTo()) ->setPickupDate($assignment->getPickupDate()) ->setPickup($assignment->getPickup()) ->setBenefits($assignment->getBenefits()) @@ -266,66 +256,6 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa return $this; } - public function getDateFrom(): ?\DateTimeImmutable - { - return $this->dateFrom; - } - - public function setDateFrom(?\DateTimeImmutable $dateFrom): static - { - $this->dateFrom = $dateFrom; - - return $this; - } - - public function getDateTo(): ?\DateTimeImmutable - { - return $this->dateTo; - } - - public function setDateTo(?\DateTimeImmutable $dateTo): static - { - $this->dateTo = $dateTo; - - return $this; - } - - /** - * Immutable on purpose: a plain CarbonPeriod hands out mutable Carbon instances, so a caller - * deriving one date from another - `$end = $period->getEndDate(); $end->addDays(14)` - moves - * the original as well and silently collapses its own period. CarbonPeriodImmutable yields - * CarbonImmutable from start, end and getEndDate(), which makes that mistake impossible. - */ - public function getEffectivePeriod(): ?CarbonPeriodImmutable - { - if (null !== $destination = $this->getDestination()) { - $dateFrom = $this->getDateFrom() ?? $destination->getDateFrom(); - $dateTo = $this->getDateTo() ?? $destination->getDateTo(); - - return new CarbonPeriodImmutable($dateFrom, $dateTo); - } - - return null; - } - - public function getEffectiveDateFrom(): ?\DateTimeImmutable - { - if (null !== $effectivePeriod = $this->getEffectivePeriod()) { - return $effectivePeriod->start->toDateTimeImmutable(); - } - - return null; - } - - public function getEffectiveDateTo(): ?\DateTimeImmutable - { - if (null !== $effectivePeriod = $this->getEffectivePeriod()) { - return $effectivePeriod->end->toDateTimeImmutable(); - } - - return null; - } - public function getEffectivePickupDate(): ?\DateTimeImmutable { if (null !== $this->pickupDate) { diff --git a/src/Entity/Disposition.php b/src/Entity/Disposition.php index 6f33a50..8924154 100644 --- a/src/Entity/Disposition.php +++ b/src/Entity/Disposition.php @@ -246,9 +246,8 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf } $assignment = $this->getAssignment(); - $assignmentPeriod = $assignment->getEffectivePeriod(); $dueDateFrom = $this->getCreatedAt()->modify(sprintf('+%d days', $deadlineDays)); - $dueDateTo = $assignmentPeriod->end->modify('-1 day'); + $dueDateTo = $assignment->getDestination()->getDateTo()->modify('-1 day'); // A short-notice assignment can end before the normal $deadlineDays window would even // start. Left uncapped, dueDateFrom >= dueDateTo makes isStarted() and isEnded() equal @@ -297,9 +296,8 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf } $assignment = $this->getAssignment(); - $assignmentPeriod = $assignment->getEffectivePeriod(); - $dueDateFrom = $assignmentPeriod->getEndDate(); - $dueDateTo = $dueDateFrom->addDays($deadlineDays); + $dueDateFrom = $assignment->getDestination()->getDateTo(); + $dueDateTo = $dueDateFrom->modify(sprintf('+%d days', $deadlineDays)); $period = CarbonPeriodImmutable::create($dueDateFrom, $dueDateTo); diff --git a/src/Entity/Feedback.php b/src/Entity/Feedback.php index efebcbf..dd70109 100644 --- a/src/Entity/Feedback.php +++ b/src/Entity/Feedback.php @@ -98,8 +98,8 @@ class Feedback implements BlameableEntityInterface, TimestampableEntityInterface $instance ->setAssignment($assignment) - ->setAssignmentDateFrom($assignment->getEffectiveDateFrom()) - ->setAssignmentDateTo($assignment->getEffectiveDateTo()) + ->setAssignmentDateFrom($destination->getDateFrom()) + ->setAssignmentDateTo($destination->getDateTo()) ->setDestinationName($destination->getProduct()) ->setHotel($destination->getHotel()) ->setHotelCode($destination->getHotelCode()) diff --git a/src/EventListener/DispositionWorkflowGuardSubscriber.php b/src/EventListener/DispositionWorkflowGuardSubscriber.php index 2f9e90a..0b1234c 100644 --- a/src/EventListener/DispositionWorkflowGuardSubscriber.php +++ b/src/EventListener/DispositionWorkflowGuardSubscriber.php @@ -75,14 +75,13 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface /** @var Disposition $disposition */ $disposition = $event->getSubject(); $assignment = $disposition->getAssignment(); - $assignmentPeriod = $assignment->getEffectivePeriod(); $today = new \DateTimeImmutable(); $dueDateFrom = $disposition ->getCreatedAt() ->modify(sprintf('+%d days', $this->contractUploadDeadlineDays)) ; - $dueDateTo = $assignmentPeriod->end->modify('-1 day'); + $dueDateTo = $assignment->getDestination()->getDateTo()->modify('-1 day'); // Block upload if deadline has passed if ($dueDateTo < $today) { diff --git a/src/EventListener/InvalidateApplicationsListener.php b/src/EventListener/InvalidateApplicationsListener.php index db12ca7..d432117 100644 --- a/src/EventListener/InvalidateApplicationsListener.php +++ b/src/EventListener/InvalidateApplicationsListener.php @@ -6,6 +6,7 @@ use App\Entity\Upload; use App\Event\ApplicationDeletedEvent; use App\Event\DocumentConfirmedEvent; use App\Repository\ApplicationRepository; +use Carbon\CarbonPeriodImmutable; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; @@ -37,10 +38,12 @@ class InvalidateApplicationsListener return; } - if (null === $period = $disposition->getAssignment()->getEffectivePeriod()) { + if (null === $destination = $disposition->getAssignment()->getDestination()) { return; } + $period = new CarbonPeriodImmutable($destination->getDateFrom(), $destination->getDateTo()); + $applications = $this->applicationRepository->findOverlappingForTeamer($teamer, $period); if (0 === count($applications)) { @@ -55,8 +58,8 @@ class InvalidateApplicationsListener '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'), + 'date_from' => $assignment->getDestination()->getDateFrom()->format('Y-m-d'), + 'date_to' => $assignment->getDestination()->getDateTo()->format('Y-m-d'), ]); $this->entityManager->remove($application); } diff --git a/src/Form/AssignmentType.php b/src/Form/AssignmentType.php index 4f7dcb3..131d727 100644 --- a/src/Form/AssignmentType.php +++ b/src/Form/AssignmentType.php @@ -79,14 +79,6 @@ class AssignmentType extends AbstractType 'rows' => 3, ], ]) -// ->add('dateFrom', DatepickerType::class, [ -// 'label' => 'abweichender Einsatztermin von', -// 'required' => false, -// ]) -// ->add('dateTo', DatepickerType::class, [ -// 'label' => 'abweichender Einsatztermin bis', -// 'required' => false, -// ]) // ->add('pickupDate', DatepickerType::class, [ // 'label' => 'abweichender Zustieg bei Busbegleitung', // 'required' => false, diff --git a/src/Repository/ApplicationRepository.php b/src/Repository/ApplicationRepository.php index 78149e7..a00bebe 100644 --- a/src/Repository/ApplicationRepository.php +++ b/src/Repository/ApplicationRepository.php @@ -163,12 +163,10 @@ class ApplicationRepository extends ServiceEntityRepository } /** - * Finds all applications of a teamer whose effective assignment period overlaps the given period. + * Finds all applications of a teamer whose 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. + * The assignment period is the date range of its destination. Boundaries count as an + * overlap: a teamer cannot end one assignment on the same day another one begins. * * @return array */ @@ -193,26 +191,8 @@ class ApplicationRepository extends ServiceEntityRepository $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') - ) - ) + $qb->expr()->lte('destination.dateFrom', ':dateTo'), + $qb->expr()->gte('destination.dateTo', ':dateFrom') )) ->setParameter('teamer', $teamer) ->setParameter('application_rejected', Application::STATUS_REJECTED) diff --git a/src/Repository/AssignmentRepository.php b/src/Repository/AssignmentRepository.php index 31b3d3b..c0c7308 100644 --- a/src/Repository/AssignmentRepository.php +++ b/src/Repository/AssignmentRepository.php @@ -73,13 +73,7 @@ class AssignmentRepository extends ServiceEntityRepository ->innerJoin('assignment.destination', 'destination') ->innerJoin('assignment.jobProfile', 'job_profile') ->where($qb->expr()->andX( - $qb->expr()->orX( - $qb->expr()->gt('destination.dateFrom', ':now'), - $qb->expr()->andX( - $qb->expr()->isNotNull('assignment.dateFrom'), - $qb->expr()->gt('assignment.dateFrom', ':now') - ) - ), + $qb->expr()->gt('destination.dateFrom', ':now'), $qb->expr()->isNull('assignment.deletedAt'), $qb->expr()->notIn('assignment.status', ':status'), $qb->expr()->in('assignment.id', $availableAssignmentsIds) @@ -399,13 +393,7 @@ class AssignmentRepository extends ServiceEntityRepository ) ->innerJoin('assignment.destination', 'destination') ->where($qb->expr()->andX( - $qb->expr()->orX( - $qb->expr()->gt('destination.dateFrom', ':now'), - $qb->expr()->andX( - $qb->expr()->isNotNull('assignment.dateFrom'), - $qb->expr()->gt('assignment.dateFrom', ':now') - ) - ), + $qb->expr()->gt('destination.dateFrom', ':now'), $qb->expr()->notIn('assignment.status', ':status'), $qb->expr()->isNull('assignment.deletedAt') )) diff --git a/src/Repository/DispositionRepository.php b/src/Repository/DispositionRepository.php index e022854..57866a1 100644 --- a/src/Repository/DispositionRepository.php +++ b/src/Repository/DispositionRepository.php @@ -11,7 +11,6 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\Query; use Doctrine\ORM\Query\Expr\Comparison; use Doctrine\ORM\Query\Expr\Join; -use Doctrine\ORM\Query\Expr\Orx; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; @@ -43,16 +42,7 @@ class DispositionRepository extends ServiceEntityRepository $qb->expr()->eq('disposition.teamer', ':teamer'), $qb->expr()->neq('assignment.status', ':assignmentStatus'), $qb->expr()->neq('disposition.status', ':dispositionStatus'), - $qb->expr()->orX( - $qb->expr()->andX( - $qb->expr()->isNull('assignment.dateTo'), - $qb->expr()->gte('destination.dateTo', ':date') - ), - $qb->expr()->andX( - $qb->expr()->isNotNull('assignment.dateTo'), - $qb->expr()->gte('assignment.dateTo', ':date') - ) - ) + $qb->expr()->gte('destination.dateTo', ':date') )) ->setParameter('teamer', $teamer) ->setParameter('assignmentStatus', Assignment::STATUS_CALLED_OFF) @@ -85,16 +75,7 @@ class DispositionRepository extends ServiceEntityRepository $qb->expr()->eq('disposition.teamer', ':teamer'), $qb->expr()->neq('assignment.status', ':assignmentStatus'), $qb->expr()->neq('disposition.status', ':dispositionStatus'), - $qb->expr()->orX( - $qb->expr()->andX( - $qb->expr()->isNull('assignment.dateTo'), - $qb->expr()->lte('destination.dateTo', ':date') - ), - $qb->expr()->andX( - $qb->expr()->isNotNull('assignment.dateTo'), - $qb->expr()->lte('assignment.dateTo', ':date') - ) - ) + $qb->expr()->lte('destination.dateTo', ':date') )) ->setParameter('teamer', $teamer) ->setParameter('assignmentStatus', Assignment::STATUS_CALLED_OFF) @@ -145,12 +126,12 @@ class DispositionRepository extends ServiceEntityRepository } /** - * The effective start date of every non-called-off disposition of a teamer, one row - * each, for the per-season tally in the info modal. + * The start date of every non-called-off disposition of a teamer, one row each, for the + * per-season tally in the info modal. * * Same teamer/called-off filtering as findRecentDispositionsByTeamer(), but unbounded - * and stripped to the one date it needs: assignment.dateFrom falling back to the - * destination's, the season convention shared with SeasonPeriodFilter. + * and stripped to the one date it needs: the destination's start, the season convention + * shared with SeasonPeriodFilter. * * @return array */ @@ -159,7 +140,7 @@ class DispositionRepository extends ServiceEntityRepository $qb = $this->createQueryBuilder('disposition'); return $qb - ->select('COALESCE(assignment.dateFrom, destination.dateFrom) AS seasonDate') + ->select('destination.dateFrom AS seasonDate') ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.destination', 'destination') ->where($qb->expr()->andX( @@ -242,13 +223,7 @@ class DispositionRepository extends ServiceEntityRepository ->leftJoin('disposition.feedback', 'feedback') ->where($qb->expr()->andX( $qb->expr()->notIn('assignment.status', ':status'), - $qb->expr()->orX( - $qb->expr()->andX( - $qb->expr()->isNotNull('assignment.dateTo'), - $qb->expr()->lte('assignment.dateTo', ':dateTo') - ), - $qb->expr()->lte('destination.dateTo', ':dateTo') - ), + $qb->expr()->lte('destination.dateTo', ':dateTo'), $qb->expr()->isNull('feedback'), $this->excludesSkipFormalities($qb) )) @@ -302,13 +277,7 @@ class DispositionRepository extends ServiceEntityRepository ->innerJoin('assignment.destination', 'destination') ->where($qb->expr()->andX( $qb->expr()->in('disposition.status', ':status'), - $qb->expr()->orX( - $qb->expr()->andX( - $qb->expr()->isNotNull('assignment.dateTo'), - $qb->expr()->lte('assignment.dateTo', ':dateTo') - ), - $qb->expr()->lte('destination.dateTo', ':dateTo') - ) + $qb->expr()->lte('destination.dateTo', ':dateTo') )) ->setParameter('status', [ Disposition::STATUS_NEW, @@ -529,10 +498,10 @@ class DispositionRepository extends ServiceEntityRepository $normalDay = $qb->expr()->andX( $qb->expr()->gte('disposition.createdAt', ':dayStart'), $qb->expr()->lt('disposition.createdAt', ':dayEnd'), - $this->effectiveDateToGreaterThan($qb, ':tomorrow'), + $qb->expr()->gt('destination.dateTo', ':tomorrow'), ); $shortNoticeDay = $qb->expr()->andX( - $this->effectiveDateToEquals($qb, ':tomorrow'), + $qb->expr()->eq('destination.dateTo', ':tomorrow'), $qb->expr()->gte('disposition.createdAt', ':dayStart'), ); @@ -599,7 +568,7 @@ class DispositionRepository extends ServiceEntityRepository // in CronCommand, so a skip-formalities placement is still `confirmed` here and // would slip past the status filter on the day after the assignment ends. $this->excludesSkipFormalities($qb), - $this->effectiveDateToEquals($qb, ':endDate'), + $qb->expr()->eq('destination.dateTo', ':endDate'), )) ->setParameter('documentType', Upload::TYPE_INVOICE) ->setParameter('dispositionStatus', [Disposition::STATUS_CALLED_OFF, Disposition::STATUS_COMPLETED]) @@ -611,30 +580,6 @@ class DispositionRepository extends ServiceEntityRepository ; } - /** - * Compares the assignment's end date against $parameter, falling back to the destination's - * when the assignment does not override it. - * - * The fallback is guarded on both sides - without the isNull() on the second branch an - * assignment that moves the end date into the future would still match on the - * destination's date. Kept in one place because the per-assignment date override is due to - * be removed (docs/remove-assignment-date-override.md), and this then collapses to a plain - * comparison on destination.dateTo. - */ - private function effectiveDateToEquals(QueryBuilder $qb, string $parameter): Orx - { - return $qb->expr()->orX( - $qb->expr()->andX( - $qb->expr()->isNotNull('assignment.dateTo'), - $qb->expr()->eq('assignment.dateTo', $parameter) - ), - $qb->expr()->andX( - $qb->expr()->isNull('assignment.dateTo'), - $qb->expr()->eq('destination.dateTo', $parameter) - ), - ); - } - /** * Restricts a query to assignments that still run the contract, invoice and feedback process. * @@ -653,23 +598,6 @@ class DispositionRepository extends ServiceEntityRepository return $qb->expr()->eq('assignment.skipFormalities', ':skipFormalities'); } - /** - * Same fallback as effectiveDateToEquals(), but for "later than $parameter". - */ - private function effectiveDateToGreaterThan(QueryBuilder $qb, string $parameter): Orx - { - return $qb->expr()->orX( - $qb->expr()->andX( - $qb->expr()->isNotNull('assignment.dateTo'), - $qb->expr()->gt('assignment.dateTo', $parameter) - ), - $qb->expr()->andX( - $qb->expr()->isNull('assignment.dateTo'), - $qb->expr()->gt('destination.dateTo', $parameter) - ), - ); - } - /** * Dispositions whose contract upload period has run out, for the admin dashboard. * diff --git a/src/Repository/Filter/SeasonPeriodFilter.php b/src/Repository/Filter/SeasonPeriodFilter.php index 40a2243..65ba0b5 100644 --- a/src/Repository/Filter/SeasonPeriodFilter.php +++ b/src/Repository/Filter/SeasonPeriodFilter.php @@ -5,15 +5,14 @@ namespace App\Repository\Filter; use Doctrine\ORM\QueryBuilder; /** - * Narrows a query to the season an assignment runs in. + * Narrows a query to the season an assignment runs in, which is the date range of its + * destination. * - * The season is assignment.dateFrom/dateTo falling back to the destination's, mirroring - * Assignment::getEffectivePeriod(), which DQL cannot call. Every statistics screen means - * this by a date range, so it lives in one place: three repositories carrying the same - * expression by hand is three chances for "a season" to come to mean something slightly - * different on one screen than on the one beside it. + * Kept in one place because every statistics screen means the same thing by "a season": + * the expression carried by hand in each repository is one more chance for it to come to + * mean something slightly different on one screen than on the one beside it. * - * Both aliases must already be joined by the caller - this only adds the conditions. + * The destination alias must already be joined by the caller - this only adds the conditions. */ final class SeasonPeriodFilter { @@ -21,37 +20,18 @@ final class SeasonPeriodFilter QueryBuilder $qb, ?\DateTimeImmutable $dateFrom, ?\DateTimeImmutable $dateTo, - string $assignmentAlias = 'assignment', string $destinationAlias = 'destination', ): void { if (null !== $dateFrom) { $qb - ->andWhere($qb->expr()->orX( - $qb->expr()->andX( - $qb->expr()->isNotNull($assignmentAlias.'.dateFrom'), - $qb->expr()->gte($assignmentAlias.'.dateFrom', ':dateFrom') - ), - $qb->expr()->andX( - $qb->expr()->isNull($assignmentAlias.'.dateFrom'), - $qb->expr()->gte($destinationAlias.'.dateFrom', ':dateFrom') - ) - )) + ->andWhere($qb->expr()->gte($destinationAlias.'.dateFrom', ':dateFrom')) ->setParameter('dateFrom', $dateFrom) ; } if (null !== $dateTo) { $qb - ->andWhere($qb->expr()->orX( - $qb->expr()->andX( - $qb->expr()->isNotNull($assignmentAlias.'.dateTo'), - $qb->expr()->lte($assignmentAlias.'.dateTo', ':dateTo') - ), - $qb->expr()->andX( - $qb->expr()->isNull($assignmentAlias.'.dateTo'), - $qb->expr()->lte($destinationAlias.'.dateTo', ':dateTo') - ) - )) + ->andWhere($qb->expr()->lte($destinationAlias.'.dateTo', ':dateTo')) ->setParameter('dateTo', $dateTo) ; } diff --git a/src/Service/Common/IcsGenerator.php b/src/Service/Common/IcsGenerator.php index c5c6d17..166449a 100644 --- a/src/Service/Common/IcsGenerator.php +++ b/src/Service/Common/IcsGenerator.php @@ -21,9 +21,11 @@ class IcsGenerator $label = sprintf('E&P: Einteilung als %s', $assignment->getJobProfile()->getName()); $address = sprintf("%s\n%s", $assignment->getDestination()->getProduct(), $assignment->getDestination()->getHotel()); + $destination = $assignment->getDestination(); + $event = Event::create($label) - ->startsAt($assignment->getEffectivePeriod()->start) - ->endsAt($assignment->getEffectivePeriod()->end->modify('+1 day')) + ->startsAt($destination->getDateFrom()) + ->endsAt($destination->getDateTo()->modify('+1 day')) ->address($address) ->description($label) ; diff --git a/src/Service/Pdf/ContractRenderer.php b/src/Service/Pdf/ContractRenderer.php index 9ec4ade..c1a2528 100644 --- a/src/Service/Pdf/ContractRenderer.php +++ b/src/Service/Pdf/ContractRenderer.php @@ -39,8 +39,8 @@ class ContractRenderer extends AbstractPdfRenderer $pdf->Text(21, 104.5, $pdf->encodeString($assignment->getJobProfile()->getName())); // Period - $dateFrom = $assignment->getEffectivePeriod()->start->format('d.m.Y'); - $dateTo = $assignment->getEffectivePeriod()->end->format('d.m.Y'); + $dateFrom = $destination->getDateFrom()->format('d.m.Y'); + $dateTo = $destination->getDateTo()->format('d.m.Y'); $period = sprintf('%s - %s', $dateFrom, $dateTo); $pdf->Text(57, 113, $period); diff --git a/src/Service/Pdf/InvoiceRenderer.php b/src/Service/Pdf/InvoiceRenderer.php index 5051001..7b4f1c4 100644 --- a/src/Service/Pdf/InvoiceRenderer.php +++ b/src/Service/Pdf/InvoiceRenderer.php @@ -37,7 +37,7 @@ class InvoiceRenderer extends AbstractPdfRenderer // Invoice number $pdf->SetFont('Arial', '', 12); - $date = $assignment->getEffectivePeriod()->start->format('d/m/Y'); + $date = $assignment->getDestination()->getDateFrom()->format('d/m/Y'); $suffix = $assignment->getId(); $number = sprintf('%s-%05d', $date, $suffix); $pdf->Text(85, 116, $number); diff --git a/src/Validator/Constraints/ApplicationValidator.php b/src/Validator/Constraints/ApplicationValidator.php index b91b720..650d04e 100644 --- a/src/Validator/Constraints/ApplicationValidator.php +++ b/src/Validator/Constraints/ApplicationValidator.php @@ -37,12 +37,12 @@ class ApplicationValidator extends ConstraintValidator return; } - if (null === $period = $application->getAssignment()->getEffectivePeriod()) { + if (null === $destination = $application->getAssignment()->getDestination()) { return; } - $applicationDateFrom = $period->start->toDateTimeImmutable(); - $applicationDateTo = $period->end->toDateTimeImmutable(); + $applicationDateFrom = $destination->getDateFrom(); + $applicationDateTo = $destination->getDateTo(); $qb = $this ->dispositionRepository @@ -62,26 +62,8 @@ class ApplicationValidator extends ConstraintValidator $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', ':application_date_to') - ), - $qb->expr()->andX( - $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') - ) - ) + $qb->expr()->lte('destination.dateFrom', ':application_date_to'), + $qb->expr()->gte('destination.dateTo', ':application_date_from') )) ->setParameter('teamer', $teamer) ->setParameter('disposition_called_off', Disposition::STATUS_CALLED_OFF) diff --git a/templates/_partials/_assignment_info.html.twig b/templates/_partials/_assignment_info.html.twig index 6b9cc37..da00339 100644 --- a/templates/_partials/_assignment_info.html.twig +++ b/templates/_partials/_assignment_info.html.twig @@ -25,7 +25,7 @@ Einsatztermin
- {{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }} + {{ assignment.destination.dateFrom|date('d.m.Y') }} - {{ assignment.destination.dateTo|date('d.m.Y') }}
{% if assignment.pickup %} diff --git a/templates/_partials/_assignment_info_compact.html.twig b/templates/_partials/_assignment_info_compact.html.twig index e3b8e83..9d752e5 100644 --- a/templates/_partials/_assignment_info_compact.html.twig +++ b/templates/_partials/_assignment_info_compact.html.twig @@ -26,7 +26,7 @@ Einsatztermin
- {{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }} + {{ assignment.destination.dateFrom|date('d.m.Y') }} - {{ assignment.destination.dateTo|date('d.m.Y') }}
diff --git a/templates/_partials/_disposition_data.html.twig b/templates/_partials/_disposition_data.html.twig index a45281c..de0ea2d 100644 --- a/templates/_partials/_disposition_data.html.twig +++ b/templates/_partials/_disposition_data.html.twig @@ -2,7 +2,7 @@ {{ disposition.teamer.fullName(true) }} {% include '_partials/_teamer_deleted_badge.html.twig' with { 'teamer': disposition.teamer } %} {% include '_partials/_dot.html.twig' %} -{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }} +{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }} {% include '_partials/_dot.html.twig' %} {{ assignment.destination.hotel }} {% include '_partials/_dot.html.twig' %} diff --git a/templates/admin/application/index.html.twig b/templates/admin/application/index.html.twig index 667d392..bb680c8 100644 --- a/templates/admin/application/index.html.twig +++ b/templates/admin/application/index.html.twig @@ -67,8 +67,8 @@ - {{ assignment.effectivePeriod.start|date('d.m.Y') }} - - {{ assignment.effectivePeriod.end|date('d.m.Y') }} + {{ assignment.destination.dateFrom|date('d.m.Y') }} - + {{ assignment.destination.dateTo|date('d.m.Y') }} diff --git a/templates/admin/index.html.twig b/templates/admin/index.html.twig index c395b59..9c62b3f 100644 --- a/templates/admin/index.html.twig +++ b/templates/admin/index.html.twig @@ -18,7 +18,7 @@ {{ icon('hand', 'w-4 h-4 shrink-0') }} {{ application.teamer.fullName(true) }} {% include '_partials/_dot.html.twig' %} - {{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }} + {{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }} {% include '_partials/_dot.html.twig' %} {{ assignment.destination.hotel }} {% include '_partials/_dot.html.twig' %} @@ -74,7 +74,7 @@ {{ icon('download', 'w-4 h-4 shrink-0') }} {{ document.owner.teamer.fullName(true) }} {% include '_partials/_dot.html.twig' %} - {{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }} + {{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }} {% include '_partials/_dot.html.twig' %} {{ assignment.destination.hotel }} @@ -107,7 +107,7 @@ hx-swap="beforeend"> {{ icon('download', 'w-4 h-4 shrink-0') }} {{ document.owner.teamer.fullName(true) }} - {{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }} + {{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }} {% include '_partials/_dot.html.twig' %} {{ assignment.destination.hotel }} @@ -162,7 +162,7 @@ {{ icon('calendar', 'w-4 h-4 shrink-0') }} - {{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }} + {{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }} {% include '_partials/_dot.html.twig' %} {{ assignment.jobProfile.name }} {% include '_partials/_dot.html.twig' %} @@ -286,7 +286,7 @@ {{ icon('calendar', 'w-4 h-4 shrink-0') }} - {{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }} + {{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }} {% include '_partials/_dot.html.twig' %} {{ assignment.jobProfile.name }} {% include '_partials/_dot.html.twig' %} diff --git a/templates/administrative/assignment/index.html.twig b/templates/administrative/assignment/index.html.twig index 318cbfe..1dac750 100644 --- a/templates/administrative/assignment/index.html.twig +++ b/templates/administrative/assignment/index.html.twig @@ -96,9 +96,9 @@ - {{ assignment.effectivePeriod.start|date('d.m.Y') }} - + {{ assignment.destination.dateFrom|date('d.m.Y') }} -
- {{ assignment.effectivePeriod.end|date('d.m.Y') }} + {{ assignment.destination.dateTo|date('d.m.Y') }}
diff --git a/templates/administrative/teamer/upcoming_assignments.html.twig b/templates/administrative/teamer/upcoming_assignments.html.twig index 579a9a2..1f8a71f 100644 --- a/templates/administrative/teamer/upcoming_assignments.html.twig +++ b/templates/administrative/teamer/upcoming_assignments.html.twig @@ -31,7 +31,7 @@ {% for disposition in pagination %} - {{ disposition.assignment.effectivePeriod.start|date('d.m.Y') }} - {{ disposition.assignment.effectivePeriod.end|date('d.m.Y') }} + {{ disposition.assignment.destination.dateFrom|date('d.m.Y') }} - {{ disposition.assignment.destination.dateTo|date('d.m.Y') }} {{ disposition.assignment.destination.product }} diff --git a/templates/house_manager/feedback/index.html.twig b/templates/house_manager/feedback/index.html.twig index ef4e5fb..e5ece56 100644 --- a/templates/house_manager/feedback/index.html.twig +++ b/templates/house_manager/feedback/index.html.twig @@ -63,9 +63,9 @@ - {{ assignment.effectivePeriod.start|date('d.m.Y') }} - + {{ assignment.destination.dateFrom|date('d.m.Y') }} -
- {{ assignment.effectivePeriod.end|date('d.m.Y') }} + {{ assignment.destination.dateTo|date('d.m.Y') }}
diff --git a/templates/house_manager/index.html.twig b/templates/house_manager/index.html.twig index 4d58729..4ae4212 100644 --- a/templates/house_manager/index.html.twig +++ b/templates/house_manager/index.html.twig @@ -12,7 +12,7 @@ {{ icon('info', 'w-4 h-4') }} {% include '_partials/_dot.html.twig' %} - {{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }} + {{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }} {% include '_partials/_dot.html.twig' %} {{ assignment.destination.hotel }} {% include '_partials/_dot.html.twig' %} diff --git a/templates/manager/index.html.twig b/templates/manager/index.html.twig index 4c4c378..422e78b 100644 --- a/templates/manager/index.html.twig +++ b/templates/manager/index.html.twig @@ -22,7 +22,7 @@ {{ icon('download', 'w-4 h-4 shrink-0') }} {{ document.owner.teamer.fullName(true) }} {% include '_partials/_dot.html.twig' %} - {{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }} + {{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }} {% include '_partials/_dot.html.twig' %} {{ assignment.destination.hotel }} @@ -54,7 +54,7 @@ hx-swap="beforeend"> {{ icon('download', 'w-4 h-4 shrink-0') }} {{ document.owner.teamer.fullName(true) }} - {{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }} + {{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }} {% include '_partials/_dot.html.twig' %} {{ assignment.destination.hotel }} diff --git a/templates/teamer/application/index.html.twig b/templates/teamer/application/index.html.twig index b014db0..e930567 100644 --- a/templates/teamer/application/index.html.twig +++ b/templates/teamer/application/index.html.twig @@ -17,7 +17,7 @@
{{ icon('calendar', 'w-5 h-5 shrink-0') }} - {{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }} + {{ assignment.destination.dateFrom|date('d.m.Y') }} - {{ assignment.destination.dateTo|date('d.m.Y') }}
{{ icon('profile', 'w-5 h-5 shrink-0') }} diff --git a/templates/teamer/assignment/index.html.twig b/templates/teamer/assignment/index.html.twig index 9d6140e..b468867 100644 --- a/templates/teamer/assignment/index.html.twig +++ b/templates/teamer/assignment/index.html.twig @@ -38,7 +38,7 @@
{{ icon('calendar', 'w-5 h-5 shrink-0') }} - {{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }} + {{ assignment.destination.dateFrom|date('d.m.Y') }} - {{ assignment.destination.dateTo|date('d.m.Y') }}
{{ icon('profile', 'w-5 h-5 shrink-0') }} diff --git a/templates/teamer/bookmark/index.html.twig b/templates/teamer/bookmark/index.html.twig index f235f70..40cdf67 100644 --- a/templates/teamer/bookmark/index.html.twig +++ b/templates/teamer/bookmark/index.html.twig @@ -16,7 +16,7 @@
{{ icon('calendar', 'w-5 h-5 shrink-0') }} - {{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }} + {{ assignment.destination.dateFrom|date('d.m.Y') }} - {{ assignment.destination.dateTo|date('d.m.Y') }}
{{ icon('profile', 'w-5 h-5 shrink-0') }} diff --git a/templates/teamer/disposition/_list.html.twig b/templates/teamer/disposition/_list.html.twig index 165d3f4..4f4bc31 100644 --- a/templates/teamer/disposition/_list.html.twig +++ b/templates/teamer/disposition/_list.html.twig @@ -9,7 +9,7 @@
{{ icon('calendar', 'w-5 h-5 shrink-0') }} - {{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }} + {{ assignment.destination.dateFrom|date('d.m.Y') }} - {{ assignment.destination.dateTo|date('d.m.Y') }}
{{ icon('profile', 'w-5 h-5 shrink-0') }} diff --git a/templates/teamer/index.html.twig b/templates/teamer/index.html.twig index 13649b4..24fceec 100644 --- a/templates/teamer/index.html.twig +++ b/templates/teamer/index.html.twig @@ -28,7 +28,7 @@
{{ icon('hand', 'w-4 h-4 shrink-0') }}
- {{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }} + {{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }} {% include '_partials/_dot.html.twig' %} {{ assignment.destination.hotel }}
@@ -56,7 +56,7 @@
{{ icon('alert', 'w-4 h-4 shrink-0') }}
- {{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }} + {{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }} {% include '_partials/_dot.html.twig' %} {{ assignment.destination.hotel }}
@@ -124,7 +124,7 @@
{{ icon('check', 'w-4 h-4 shrink-0') }}
- {{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }} + {{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }} {% include '_partials/_dot.html.twig' %} {{ assignment.destination.hotel }}
@@ -149,7 +149,7 @@
{{ icon('calendar', 'w-4 h-4 shrink-0') }} - {{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }} + {{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }} {% include '_partials/_dot.html.twig' %} {{ assignment.destination.hotel }}
diff --git a/tests/EventListener/InvalidateApplicationsListenerTest.php b/tests/EventListener/InvalidateApplicationsListenerTest.php index 3ca2e72..f6cc923 100644 --- a/tests/EventListener/InvalidateApplicationsListenerTest.php +++ b/tests/EventListener/InvalidateApplicationsListenerTest.php @@ -95,7 +95,7 @@ class InvalidateApplicationsListenerTest extends TestCase $this->assertSame([], $this->removedApplications); } - public function testIgnoresAssignmentsWithoutEffectivePeriod(): void + public function testIgnoresAssignmentsWithoutDestination(): void { $document = $this->createContract(null, $this->createTeamer()); @@ -125,7 +125,7 @@ class InvalidateApplicationsListenerTest extends TestCase $this->assertSame([], $this->dispatchedEvents); } - public function testQueriesOverlappingApplicationsForTheTeamerAndTheEffectivePeriod(): void + public function testQueriesOverlappingApplicationsForTheTeamerAndTheDestinationPeriod(): void { $teamer = $this->createTeamer(); $period = new CarbonPeriodImmutable('2025-01-10', '2025-01-20'); @@ -134,7 +134,13 @@ class InvalidateApplicationsListenerTest extends TestCase $this->applicationRepository ->expects($this->once()) ->method('findOverlappingForTeamer') - ->with($teamer, $period) + ->with( + $this->identicalTo($teamer), + $this->callback(static function (CarbonPeriodImmutable $actual) use ($period): bool { + return $actual->getStartDate()->equalTo($period->getStartDate()) + && $actual->getEndDate()->equalTo($period->getEndDate()); + }) + ) ->willReturn([]) ; @@ -194,7 +200,15 @@ class InvalidateApplicationsListenerTest extends TestCase private function createContract(?CarbonPeriodImmutable $period, Teamer $teamer): Upload&MockObject { $assignment = $this->createMock(Assignment::class); - $assignment->method('getEffectivePeriod')->willReturn($period); + + if (null === $period) { + $assignment->method('getDestination')->willReturn(null); + } else { + $destination = $this->createMock(Destination::class); + $destination->method('getDateFrom')->willReturn($period->getStartDate()->toDateTimeImmutable()); + $destination->method('getDateTo')->willReturn($period->getEndDate()->toDateTimeImmutable()); + $assignment->method('getDestination')->willReturn($destination); + } $disposition = $this->createMock(Disposition::class); $disposition->method('getAssignment')->willReturn($assignment); @@ -211,12 +225,12 @@ class InvalidateApplicationsListenerTest extends TestCase { $destination = $this->createMock(Destination::class); $destination->method('getHotelCode')->willReturn('TST'); + $destination->method('getDateFrom')->willReturn(new \DateTimeImmutable('2025-01-12')); + $destination->method('getDateTo')->willReturn(new \DateTimeImmutable('2025-01-18')); $assignment = $this->createMock(Assignment::class); $assignment->method('getDestination')->willReturn($destination); $assignment->method('getUuid')->willReturn('assignment-uuid'); - $assignment->method('getEffectiveDateFrom')->willReturn(new \DateTimeImmutable('2025-01-12')); - $assignment->method('getEffectiveDateTo')->willReturn(new \DateTimeImmutable('2025-01-18')); $application = $this->createMock(Application::class); $application->method('getUuid')->willReturn('application-uuid'); diff --git a/tests/Repository/ApplicationRepositoryTest.php b/tests/Repository/ApplicationRepositoryTest.php index 812e910..b16b057 100644 --- a/tests/Repository/ApplicationRepositoryTest.php +++ b/tests/Repository/ApplicationRepositoryTest.php @@ -20,21 +20,13 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; */ class ApplicationRepositoryTest extends KernelTestCase { - public function testOverlappingQueryUsesInclusiveBoundariesAndEffectiveDates(): void + public function testOverlappingQueryUsesInclusiveBoundariesOnTheDestinationDates(): void { $dql = $this->createOverlappingQuery()->getDQL(); - // an assignment overrides the date range of its destination, so both have to be considered - $this->assertStringContainsString( - '(assignment.dateFrom IS NULL AND destination.dateFrom <= :dateTo)' - .' OR (assignment.dateFrom IS NOT NULL AND assignment.dateFrom <= :dateTo)', - $dql - ); - $this->assertStringContainsString( - '(assignment.dateTo IS NULL AND destination.dateTo >= :dateFrom)' - .' OR (assignment.dateTo IS NOT NULL AND assignment.dateTo >= :dateFrom)', - $dql - ); + // 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 diff --git a/tests/Repository/DispositionRepositoryTest.php b/tests/Repository/DispositionRepositoryTest.php index e084931..bf99024 100644 --- a/tests/Repository/DispositionRepositoryTest.php +++ b/tests/Repository/DispositionRepositoryTest.php @@ -93,14 +93,8 @@ class DispositionRepositoryTest extends KernelTestCase { $dql = $this->contractQuery()->getDQL(); - $this->assertStringContainsString( - 'assignment.dateTo IS NOT NULL AND assignment.dateTo > :tomorrow', - $dql - ); - $this->assertStringContainsString( - 'assignment.dateTo IS NOT NULL AND assignment.dateTo = :tomorrow', - $dql - ); + $this->assertStringContainsString('destination.dateTo > :tomorrow', $dql); + $this->assertStringContainsString('destination.dateTo = :tomorrow', $dql); } public function testContractReminderBindsTomorrowAsADateAndNotATimestamp(): void @@ -114,8 +108,8 @@ class DispositionRepositoryTest extends KernelTestCase } /** - * The bug that killed this query: destination.dateTo and assignment.dateTo are DATE columns, - * and Doctrine binds a DateTimeImmutable as 'Y-m-d H:i:s', which a DATE never equals. + * The bug that killed this query: destination.dateTo is a DATE column, and Doctrine binds a + * DateTimeImmutable as 'Y-m-d H:i:s', which a DATE never equals. */ public function testInvoiceReminderBindsADateAndNotATimestamp(): void { @@ -129,21 +123,13 @@ class DispositionRepositoryTest extends KernelTestCase } /** - * Without the IS NULL guard on the second branch, an assignment that moves the end date into - * the future still matches on its destination's date. + * The reminder fires for placements whose destination ends on exactly $endDate. */ - public function testInvoiceReminderGuardsTheDestinationFallback(): void + public function testInvoiceReminderComparesTheDestinationEndDate(): void { $dql = $this->invoiceQuery()->getDQL(); - $this->assertStringContainsString( - 'assignment.dateTo IS NOT NULL AND assignment.dateTo = :endDate', - $dql - ); - $this->assertStringContainsString( - 'assignment.dateTo IS NULL AND destination.dateTo = :endDate', - $dql - ); + $this->assertStringContainsString('destination.dateTo = :endDate', $dql); } public function testInvoiceReminderOnlyJoinsTheInvoiceAndSkipsFinishedDispositions(): void diff --git a/tests/Repository/StatisticsEventRepositoryTest.php b/tests/Repository/StatisticsEventRepositoryTest.php index fb2e531..708f913 100644 --- a/tests/Repository/StatisticsEventRepositoryTest.php +++ b/tests/Repository/StatisticsEventRepositoryTest.php @@ -24,25 +24,21 @@ class StatisticsEventRepositoryTest extends KernelTestCase { $dql = $this->query(dateFrom: new \DateTimeImmutable('2026-07-01'))->getDQL(); - $this->assertStringContainsString('assignment.dateFrom >= :dateFrom', $dql); + $this->assertStringContainsString('destination.dateFrom >= :dateFrom', $dql); $this->assertStringNotContainsString('event.occurredAt', $dql); } /** - * The season start is assignment.dateFrom falling back to the destination's, per - * Assignment::getEffectivePeriod(). Losing the fallback would silently drop every - * assignment that inherits its dates. + * The season is the destination's date range, the convention shared with SeasonPeriodFilter. */ - public function testTheSeasonFallsBackToTheDestinationDates(): void + public function testTheSeasonIsTheDestinationDateRange(): void { $dql = $this->query( dateFrom: new \DateTimeImmutable('2026-07-01'), dateTo: new \DateTimeImmutable('2027-06-30'), )->getDQL(); - $this->assertStringContainsString('assignment.dateFrom IS NULL', $dql); $this->assertStringContainsString('destination.dateFrom >= :dateFrom', $dql); - $this->assertStringContainsString('assignment.dateTo IS NULL', $dql); $this->assertStringContainsString('destination.dateTo <= :dateTo', $dql); } diff --git a/tests/Service/Pdf/InvoiceRendererTest.php b/tests/Service/Pdf/InvoiceRendererTest.php index ea8b0a9..96dd1b1 100644 --- a/tests/Service/Pdf/InvoiceRendererTest.php +++ b/tests/Service/Pdf/InvoiceRendererTest.php @@ -17,7 +17,6 @@ use App\Service\Pdf\InvoiceApprover; use App\Service\Pdf\InvoiceRenderer; use App\Service\Pdf\Pdf; use App\Service\Upload\UploadHandler; -use Carbon\CarbonPeriodImmutable; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Contracts\Translation\TranslatorInterface; @@ -84,7 +83,6 @@ class InvoiceRendererTest extends WebTestCase ; $assignment = $this->createMock(Assignment::class); - $assignment->method('getEffectivePeriod')->willReturn(CarbonPeriodImmutable::create('2025-02-01', '2025-02-08')); $assignment->method('getId')->willReturn(1234); $assignment->method('getDestination')->willReturn($destination); $assignment->method('getJobProfile')->willReturn($jobProfile);