creerInvitation(); self::assertInstanceOf(ParentInvitationId::class, $invitation->id); self::assertTrue(TenantId::fromString(self::TENANT_ID)->equals($invitation->tenantId)); self::assertTrue(UserId::fromString(self::STUDENT_ID)->equals($invitation->studentId)); self::assertSame(self::PARENT_EMAIL, (string) $invitation->parentEmail); self::assertSame(self::CODE, (string) $invitation->code); self::assertSame(InvitationStatus::PENDING, $invitation->status); self::assertNull($invitation->sentAt); self::assertNull($invitation->activatedAt); self::assertNull($invitation->activatedUserId); } #[Test] public function creerSetsExpirationTo7DaysAfterCreation(): void { $createdAt = new DateTimeImmutable('2026-02-20 10:00:00'); $expectedExpiration = new DateTimeImmutable('2026-02-27 10:00:00'); $invitation = $this->creerInvitation(createdAt: $createdAt); self::assertEquals($expectedExpiration, $invitation->expiresAt); } #[Test] public function envoyerChangesStatusToSent(): void { $invitation = $this->creerInvitation(); $sentAt = new DateTimeImmutable('2026-02-20 11:00:00'); $invitation->envoyer($sentAt); self::assertSame(InvitationStatus::SENT, $invitation->status); self::assertEquals($sentAt, $invitation->sentAt); } #[Test] public function envoyerRecordsInvitationParentEnvoyeeEvent(): void { $invitation = $this->creerInvitation(); $sentAt = new DateTimeImmutable('2026-02-20 11:00:00'); $invitation->envoyer($sentAt); $events = $invitation->pullDomainEvents(); self::assertCount(1, $events); self::assertInstanceOf(InvitationParentEnvoyee::class, $events[0]); } #[Test] public function envoyerThrowsExceptionWhenAlreadyActivated(): void { $invitation = $this->creerInvitationActivee(); $this->expectException(InvitationDejaActiveeException::class); $invitation->envoyer(new DateTimeImmutable('2026-02-21 10:00:00')); } #[Test] public function activerChangesStatusToActivated(): void { $invitation = $this->creerInvitationEnvoyee(); $parentUserId = UserId::generate(); $activatedAt = new DateTimeImmutable('2026-02-21 10:00:00'); $invitation->activer($parentUserId, $activatedAt); self::assertSame(InvitationStatus::ACTIVATED, $invitation->status); self::assertEquals($activatedAt, $invitation->activatedAt); self::assertTrue($parentUserId->equals($invitation->activatedUserId)); } #[Test] public function activerRecordsInvitationParentActiveeEvent(): void { $invitation = $this->creerInvitationEnvoyee(); $parentUserId = UserId::generate(); $activatedAt = new DateTimeImmutable('2026-02-21 10:00:00'); $invitation->pullDomainEvents(); $invitation->activer($parentUserId, $activatedAt); $events = $invitation->pullDomainEvents(); self::assertCount(1, $events); self::assertInstanceOf(InvitationParentActivee::class, $events[0]); } #[Test] public function activerThrowsExceptionWhenAlreadyActivated(): void { $invitation = $this->creerInvitationActivee(); $this->expectException(InvitationDejaActiveeException::class); $invitation->activer(UserId::generate(), new DateTimeImmutable('2026-02-22 10:00:00')); } #[Test] public function activerThrowsExceptionWhenNotSent(): void { $invitation = $this->creerInvitation(); $this->expectException(InvitationNonEnvoyeeException::class); $invitation->activer(UserId::generate(), new DateTimeImmutable('2026-02-21 10:00:00')); } #[Test] public function activerThrowsExceptionWhenExpired(): void { $invitation = $this->creerInvitationEnvoyee( createdAt: new DateTimeImmutable('2026-02-01 10:00:00'), ); $this->expectException(InvitationExpireeException::class); $invitation->activer( UserId::generate(), new DateTimeImmutable('2026-02-20 10:00:00'), ); } #[Test] public function marquerExpireeChangesStatusWhenSent(): void { $invitation = $this->creerInvitationEnvoyee(); $invitation->marquerExpiree(); self::assertSame(InvitationStatus::EXPIRED, $invitation->status); } #[Test] public function marquerExpireeDoesNothingWhenPending(): void { $invitation = $this->creerInvitation(); $invitation->marquerExpiree(); self::assertSame(InvitationStatus::PENDING, $invitation->status); } #[Test] public function marquerExpireeDoesNothingWhenActivated(): void { $invitation = $this->creerInvitationActivee(); $invitation->marquerExpiree(); self::assertSame(InvitationStatus::ACTIVATED, $invitation->status); } #[Test] public function renvoyerResetsCodeAndStatusAndExpiration(): void { $invitation = $this->creerInvitationEnvoyee( createdAt: new DateTimeImmutable('2026-02-01 10:00:00'), ); $nouveauCode = new InvitationCode('11111111111111111111111111111111'); $renvoyeAt = new DateTimeImmutable('2026-02-15 10:00:00'); $expectedExpiration = new DateTimeImmutable('2026-02-22 10:00:00'); $invitation->renvoyer($nouveauCode, $renvoyeAt); self::assertSame(InvitationStatus::SENT, $invitation->status); self::assertSame('11111111111111111111111111111111', (string) $invitation->code); self::assertEquals($renvoyeAt, $invitation->sentAt); self::assertEquals($expectedExpiration, $invitation->expiresAt); } #[Test] public function renvoyerThrowsExceptionWhenActivated(): void { $invitation = $this->creerInvitationActivee(); $this->expectException(InvitationDejaActiveeException::class); $invitation->renvoyer( new InvitationCode('11111111111111111111111111111111'), new DateTimeImmutable('2026-02-21 10:00:00'), ); } #[Test] public function renvoyerWorksOnExpiredInvitation(): void { $invitation = $this->creerInvitationEnvoyee( createdAt: new DateTimeImmutable('2026-02-01 10:00:00'), ); $invitation->marquerExpiree(); $nouveauCode = new InvitationCode('22222222222222222222222222222222'); $renvoyeAt = new DateTimeImmutable('2026-02-15 10:00:00'); $invitation->renvoyer($nouveauCode, $renvoyeAt); self::assertSame(InvitationStatus::SENT, $invitation->status); } #[Test] public function estExpireeReturnsFalseBeforeExpiration(): void { $invitation = $this->creerInvitation( createdAt: new DateTimeImmutable('2026-02-20 10:00:00'), ); self::assertFalse($invitation->estExpiree(new DateTimeImmutable('2026-02-25 10:00:00'))); } #[Test] public function estExpireeReturnsTrueAfterExpiration(): void { $invitation = $this->creerInvitation( createdAt: new DateTimeImmutable('2026-02-20 10:00:00'), ); self::assertTrue($invitation->estExpiree(new DateTimeImmutable('2026-02-28 10:00:00'))); } #[Test] public function estExpireeReturnsTrueAtExactExpiration(): void { $invitation = $this->creerInvitation( createdAt: new DateTimeImmutable('2026-02-20 10:00:00'), ); self::assertTrue($invitation->estExpiree(new DateTimeImmutable('2026-02-27 10:00:00'))); } #[Test] public function estActiveeReturnsFalseForNewInvitation(): void { $invitation = $this->creerInvitation(); self::assertFalse($invitation->estActivee()); } #[Test] public function estActiveeReturnsTrueAfterActivation(): void { $invitation = $this->creerInvitationActivee(); self::assertTrue($invitation->estActivee()); } #[Test] public function reconstitutePreservesAllProperties(): void { $id = ParentInvitationId::generate(); $tenantId = TenantId::fromString(self::TENANT_ID); $studentId = UserId::fromString(self::STUDENT_ID); $parentEmail = new Email(self::PARENT_EMAIL); $code = new InvitationCode(self::CODE); $createdAt = new DateTimeImmutable('2026-02-20 10:00:00'); $createdBy = UserId::fromString(self::CREATED_BY_ID); $sentAt = new DateTimeImmutable('2026-02-20 11:00:00'); $activatedAt = new DateTimeImmutable('2026-02-21 10:00:00'); $activatedUserId = UserId::generate(); $invitation = ParentInvitation::reconstitute( id: $id, tenantId: $tenantId, studentId: $studentId, parentEmail: $parentEmail, code: $code, status: InvitationStatus::ACTIVATED, expiresAt: new DateTimeImmutable('2026-02-27 10:00:00'), createdAt: $createdAt, createdBy: $createdBy, sentAt: $sentAt, activatedAt: $activatedAt, activatedUserId: $activatedUserId, ); self::assertTrue($id->equals($invitation->id)); self::assertTrue($tenantId->equals($invitation->tenantId)); self::assertTrue($studentId->equals($invitation->studentId)); self::assertTrue($parentEmail->equals($invitation->parentEmail)); self::assertTrue($code->equals($invitation->code)); self::assertSame(InvitationStatus::ACTIVATED, $invitation->status); self::assertEquals($sentAt, $invitation->sentAt); self::assertEquals($activatedAt, $invitation->activatedAt); self::assertTrue($activatedUserId->equals($invitation->activatedUserId)); } private function creerInvitation(?DateTimeImmutable $createdAt = null): ParentInvitation { return ParentInvitation::creer( tenantId: TenantId::fromString(self::TENANT_ID), studentId: UserId::fromString(self::STUDENT_ID), parentEmail: new Email(self::PARENT_EMAIL), code: new InvitationCode(self::CODE), createdAt: $createdAt ?? new DateTimeImmutable('2026-02-20 10:00:00'), createdBy: UserId::fromString(self::CREATED_BY_ID), ); } private function creerInvitationEnvoyee(?DateTimeImmutable $createdAt = null): ParentInvitation { $invitation = $this->creerInvitation($createdAt); $invitation->envoyer($createdAt ?? new DateTimeImmutable('2026-02-20 10:30:00')); return $invitation; } private function creerInvitationActivee(): ParentInvitation { $invitation = $this->creerInvitationEnvoyee(); $invitation->activer( UserId::generate(), new DateTimeImmutable('2026-02-21 10:00:00'), ); return $invitation; } }