diff --git a/src/Bundle/ChillTicketBundle/src/Entity/CallerHistory.php b/src/Bundle/ChillTicketBundle/src/Entity/CallerHistory.php new file mode 100644 index 000000000..08199919b --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Entity/CallerHistory.php @@ -0,0 +1,149 @@ + CallerHistory::class])] +class CallerHistory implements TrackCreationInterface +{ + use TrackCreationTrait; + + #[ORM\Id] + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[Serializer\Groups(['read'])] + private ?int $id = null; + + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])] + #[Serializer\Groups(['read'])] + private ?\DateTimeImmutable $endDate = null; + + #[ORM\ManyToOne(targetEntity: Person::class)] + #[ORM\JoinColumn(nullable: true)] + #[Serializer\Groups(['read'])] + private ?Person $person = null; + + #[ORM\ManyToOne(targetEntity: ThirdParty::class)] + #[ORM\JoinColumn(nullable: true)] + #[Serializer\Groups(['read'])] + private ?ThirdParty $thirdParty = null; + + public function __construct( + ThirdParty|Person $caller, + #[ORM\ManyToOne(targetEntity: Ticket::class)] + #[ORM\JoinColumn(nullable: false)] + private Ticket $ticket, + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)] + #[Serializer\Groups(['read'])] + private \DateTimeImmutable $startDate = new \DateTimeImmutable('now'), + ) { + $this->setCaller($caller); + $ticket->addCallerHistory($this); + } + + public function getEndDate(): ?\DateTimeImmutable + { + return $this->endDate; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getPerson(): ?Person + { + return $this->person; + } + + public function getThirdParty(): ?ThirdParty + { + return $this->thirdParty; + } + + public function getStartDate(): \DateTimeImmutable + { + return $this->startDate; + } + + public function getTicket(): Ticket + { + return $this->ticket; + } + + public function setEndDate(?\DateTimeImmutable $endDate): void + { + $this->endDate = $endDate; + } + + public function setPerson(?Person $person): self + { + $this->person = $person; + + // If setting a person, ensure thirdParty is null + if (null !== $person) { + $this->thirdParty = null; + } + + return $this; + } + + public function setThirdParty(?ThirdParty $thirdParty): self + { + $this->thirdParty = $thirdParty; + + // If setting a thirdParty, ensure person is null + if (null !== $thirdParty) { + $this->person = null; + } + + return $this; + } + + /** + * Set the caller. + * + * This is a private method and should be only called while instance creation + */ + private function setCaller(Person|ThirdParty $caller): void + { + if ($caller instanceof Person) { + $this->setPerson($caller); + + } else { + $this->setThirdParty($caller); + } + } + + /** + * Get the caller, which can be either a Person or a ThirdParty. + */ + public function getCaller(): Person|ThirdParty + { + return $this->person ?? $this->thirdParty; + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php b/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php index 8ef4f64a9..092aa5a35 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php @@ -96,6 +96,12 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface #[ORM\OneToMany(targetEntity: EmergencyStatusHistory::class, mappedBy: 'ticket', cascade: ['persist', 'remove'])] private Collection $emergencyStatusHistories; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: CallerHistory::class, mappedBy: 'ticket', cascade: ['persist', 'remove'])] + private Collection $callerHistories; + public function __construct() { $this->addresseeHistory = new ArrayCollection(); @@ -105,6 +111,7 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface $this->inputHistories = new ArrayCollection(); $this->stateHistories = new ArrayCollection(); $this->emergencyStatusHistories = new ArrayCollection(); + $this->callerHistories = new ArrayCollection(); } public function getId(): ?int @@ -294,4 +301,36 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface { return $this->emergencyStatusHistories; } + + /** + * @internal use @see{CallerHistory::__construct} instead + */ + public function addCallerHistory(CallerHistory $callerHistory): void + { + $this->callerHistories->add($callerHistory); + } + + /** + * Get the current caller (Person or ThirdParty) associated with this ticket. + * + * @return Person|ThirdParty|null + */ + public function getCaller() + { + foreach ($this->callerHistories as $callerHistory) { + if (null === $callerHistory->getEndDate()) { + return $callerHistory->getCaller(); + } + } + + return null; + } + + /** + * @return ReadableCollection + */ + public function getCallerHistories(): ReadableCollection + { + return $this->callerHistories; + } } diff --git a/src/Bundle/ChillTicketBundle/src/migrations/Version20250624105842.php b/src/Bundle/ChillTicketBundle/src/migrations/Version20250624105842.php new file mode 100644 index 000000000..1a49a4f5b --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/migrations/Version20250624105842.php @@ -0,0 +1,93 @@ +addSql(<<<'SQL' + CREATE SEQUENCE chill_ticket.caller_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1 + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE chill_ticket.caller_history (id INT NOT NULL, person_id INT DEFAULT NULL, ticket_id INT NOT NULL, endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, thirdParty_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, PRIMARY KEY(id)) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_AD0DCE24217BBB47 ON chill_ticket.caller_history (person_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_AD0DCE243EA5CAB0 ON chill_ticket.caller_history (thirdParty_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_AD0DCE24700047D2 ON chill_ticket.caller_history (ticket_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_AD0DCE243174800F ON chill_ticket.caller_history (createdBy_id) + SQL); + $this->addSql(<<<'SQL' + COMMENT ON COLUMN chill_ticket.caller_history.endDate IS '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + COMMENT ON COLUMN chill_ticket.caller_history.startDate IS '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + COMMENT ON COLUMN chill_ticket.caller_history.createdAt IS '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history ADD CONSTRAINT FK_AD0DCE24217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history ADD CONSTRAINT FK_AD0DCE243EA5CAB0 FOREIGN KEY (thirdParty_id) REFERENCES chill_3party.third_party (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history ADD CONSTRAINT FK_AD0DCE24700047D2 FOREIGN KEY (ticket_id) REFERENCES chill_ticket.ticket (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history ADD CONSTRAINT FK_AD0DCE243174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history ADD CONSTRAINT caller_history_not_overlaps + exclude using gist (ticket_id with =, tsrange(startdate, enddate) with &&) + deferrable initially deferred + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP SEQUENCE chill_ticket.caller_history_id_seq CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history DROP CONSTRAINT FK_AD0DCE24217BBB47 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history DROP CONSTRAINT FK_AD0DCE243EA5CAB0 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history DROP CONSTRAINT FK_AD0DCE24700047D2 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.caller_history DROP CONSTRAINT FK_AD0DCE243174800F + SQL); + $this->addSql(<<<'SQL' + DROP TABLE chill_ticket.caller_history + SQL); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Entity/CallerHistoryTest.php b/src/Bundle/ChillTicketBundle/tests/Entity/CallerHistoryTest.php new file mode 100644 index 000000000..36024fde5 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Entity/CallerHistoryTest.php @@ -0,0 +1,63 @@ +getTicket()); + self::assertNull($callerHistory->getEndDate()); + self::assertSame($person, $callerHistory->getPerson()); + self::assertNull($callerHistory->getThirdParty()); + self::assertSame($person, $callerHistory->getCaller()); + } + + public function testConstructorWithThirdParty(): void + { + $ticket = new Ticket(); + + $callerHistory = new CallerHistory($thirdParty = new ThirdParty(), $ticket); + + self::assertSame($ticket, $callerHistory->getTicket()); + self::assertNull($callerHistory->getEndDate()); + self::assertNull($callerHistory->getPerson()); + self::assertSame($thirdParty, $callerHistory->getThirdParty()); + self::assertSame($thirdParty, $callerHistory->getCaller()); + } + + public function testSetEndDate(): void + { + $ticket = $this->createMock(Ticket::class); + $callerHistory = new CallerHistory(new ThirdParty(), $ticket); + + $endDate = new \DateTimeImmutable('2023-01-01'); + $callerHistory->setEndDate($endDate); + + self::assertSame($endDate, $callerHistory->getEndDate()); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Entity/TicketCallerTest.php b/src/Bundle/ChillTicketBundle/tests/Entity/TicketCallerTest.php new file mode 100644 index 000000000..ad9b8ecaf --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Entity/TicketCallerTest.php @@ -0,0 +1,82 @@ +getCaller()); + + // Create a person + $person = new Person(); + + // Create a caller history with the person + $callerHistory = new CallerHistory($person, $ticket); + + // The ticket should now return the person as the caller + self::assertSame($person, $ticket->getCaller()); + + // Create a third party + $thirdParty = new ThirdParty(); + + // Create a new caller history with the third party + $callerHistory2 = new CallerHistory($thirdParty, $ticket); + + // End the first caller history + $callerHistory->setEndDate(new \DateTimeImmutable()); + + // The ticket should now return the third party as the caller + self::assertSame($thirdParty, $ticket->getCaller()); + + // End the second caller history + $callerHistory2->setEndDate(new \DateTimeImmutable()); + + // The ticket should now return null as there is no active caller + self::assertNull($ticket->getCaller()); + } + + public function testGetCallerHistories(): void + { + $ticket = new Ticket(); + + // Initially, there should be no caller histories + self::assertCount(0, $ticket->getCallerHistories()); + + // Create a caller history + $callerHistory = new CallerHistory(new Person(), $ticket); + + // The ticket should now have one caller history + self::assertCount(1, $ticket->getCallerHistories()); + self::assertSame($callerHistory, $ticket->getCallerHistories()->first()); + + // Create another caller history + $callerHistory2 = new CallerHistory(new ThirdParty(), $ticket); + + // The ticket should now have two caller histories + self::assertCount(2, $ticket->getCallerHistories()); + } +}