calendarRepository = new InMemorySchoolCalendarRepository(); $rulesChecker = new class implements HomeworkRulesChecker { public function verifier( TenantId $tenantId, DateTimeImmutable $dueDate, DateTimeImmutable $creationDate, ): HomeworkRulesCheckResult { return HomeworkRulesCheckResult::ok(); } }; $clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-03-01 10:00:00'); } }; $this->handler = new GetBlockedDatesHandler( $this->calendarRepository, $rulesChecker, $clock, ); } #[Test] public function returnsWeekendsAsBlocked(): void { // 2026-03-02 est un lundi, 2026-03-08 est un dimanche $result = ($this->handler)(new GetBlockedDatesQuery( tenantId: self::TENANT_ID, academicYearId: self::ACADEMIC_YEAR_ID, startDate: '2026-03-02', endDate: '2026-03-08', )); $weekendDates = array_filter($result, static fn ($d) => $d->type === 'weekend'); self::assertCount(2, $weekendDates); $dates = array_map(static fn ($d) => $d->date, array_values($weekendDates)); self::assertContains('2026-03-07', $dates); self::assertContains('2026-03-08', $dates); } #[Test] public function returnsHolidaysAsBlocked(): void { $calendar = $this->createCalendarWithHoliday( new DateTimeImmutable('2026-03-04'), 'Jour de test', ); $this->calendarRepository->save($calendar); $result = ($this->handler)(new GetBlockedDatesQuery( tenantId: self::TENANT_ID, academicYearId: self::ACADEMIC_YEAR_ID, startDate: '2026-03-02', endDate: '2026-03-06', )); $holidays = array_filter($result, static fn ($d) => $d->type === CalendarEntryType::HOLIDAY->value); self::assertCount(1, $holidays); $holiday = array_values($holidays)[0]; self::assertSame('2026-03-04', $holiday->date); self::assertSame('Jour de test', $holiday->reason); } #[Test] public function returnsEmptyForWeekWithNoBlockedDates(): void { // Lundi à vendredi sans calendrier configuré $result = ($this->handler)(new GetBlockedDatesQuery( tenantId: self::TENANT_ID, academicYearId: self::ACADEMIC_YEAR_ID, startDate: '2026-03-02', endDate: '2026-03-06', )); self::assertEmpty($result); } #[Test] public function returnsVacationsAsBlocked(): void { $calendar = $this->createCalendarWithVacation( new DateTimeImmutable('2026-03-02'), new DateTimeImmutable('2026-03-06'), 'Vacances de printemps', ); $this->calendarRepository->save($calendar); $result = ($this->handler)(new GetBlockedDatesQuery( tenantId: self::TENANT_ID, academicYearId: self::ACADEMIC_YEAR_ID, startDate: '2026-03-02', endDate: '2026-03-06', )); $vacations = array_filter($result, static fn ($d) => $d->type === CalendarEntryType::VACATION->value); self::assertCount(5, $vacations); } #[Test] public function returnsRuleHardBlockedDates(): void { $rulesChecker = new class implements HomeworkRulesChecker { public function verifier( TenantId $tenantId, DateTimeImmutable $dueDate, DateTimeImmutable $creationDate, ): HomeworkRulesCheckResult { // Block Tuesday March 3 if ($dueDate->format('Y-m-d') === '2026-03-03') { return new HomeworkRulesCheckResult( warnings: [new \App\Scolarite\Application\Port\RuleWarning('minimum_delay', 'Délai minimum non respecté')], bloquant: true, ); } return HomeworkRulesCheckResult::ok(); } }; $clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-03-01 10:00:00'); } }; $handler = new GetBlockedDatesHandler($this->calendarRepository, $rulesChecker, $clock); $result = ($handler)(new GetBlockedDatesQuery( tenantId: self::TENANT_ID, academicYearId: self::ACADEMIC_YEAR_ID, startDate: '2026-03-02', endDate: '2026-03-06', )); $ruleBlocked = array_filter($result, static fn ($d) => $d->type === 'rule_hard'); self::assertCount(1, $ruleBlocked); $blocked = array_values($ruleBlocked)[0]; self::assertSame('2026-03-03', $blocked->date); self::assertSame('Délai minimum non respecté', $blocked->reason); } #[Test] public function returnsRuleSoftWarningDates(): void { $rulesChecker = new class implements HomeworkRulesChecker { public function verifier( TenantId $tenantId, DateTimeImmutable $dueDate, DateTimeImmutable $creationDate, ): HomeworkRulesCheckResult { if ($dueDate->format('Y-m-d') === '2026-03-04') { return new HomeworkRulesCheckResult( warnings: [new \App\Scolarite\Application\Port\RuleWarning('no_monday_after', 'Devoirs pour lundi déconseillés')], bloquant: false, ); } return HomeworkRulesCheckResult::ok(); } }; $clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-03-01 10:00:00'); } }; $handler = new GetBlockedDatesHandler($this->calendarRepository, $rulesChecker, $clock); $result = ($handler)(new GetBlockedDatesQuery( tenantId: self::TENANT_ID, academicYearId: self::ACADEMIC_YEAR_ID, startDate: '2026-03-02', endDate: '2026-03-06', )); $ruleSoft = array_filter($result, static fn ($d) => $d->type === 'rule_soft'); self::assertCount(1, $ruleSoft); $soft = array_values($ruleSoft)[0]; self::assertSame('2026-03-04', $soft->date); self::assertSame('rule_soft', $soft->type); } private function createCalendarWithHoliday(DateTimeImmutable $date, string $label): SchoolCalendar { $tenantId = TenantId::fromString(self::TENANT_ID); $academicYearId = AcademicYearId::fromString(self::ACADEMIC_YEAR_ID); $calendar = SchoolCalendar::initialiser($tenantId, $academicYearId); $calendar->ajouterEntree(new CalendarEntry( id: CalendarEntryId::generate(), type: CalendarEntryType::HOLIDAY, startDate: $date, endDate: $date, label: $label, )); return $calendar; } private function createCalendarWithVacation( DateTimeImmutable $startDate, DateTimeImmutable $endDate, string $label, ): SchoolCalendar { $tenantId = TenantId::fromString(self::TENANT_ID); $academicYearId = AcademicYearId::fromString(self::ACADEMIC_YEAR_ID); $calendar = SchoolCalendar::initialiser($tenantId, $academicYearId); $calendar->ajouterEntree(new CalendarEntry( id: CalendarEntryId::generate(), type: CalendarEntryType::VACATION, startDate: $startDate, endDate: $endDate, label: $label, )); return $calendar; } }