diff --git a/src/Bundle/ChillTicketBundle/src/Entity/StateEnum.php b/src/Bundle/ChillTicketBundle/src/Entity/StateEnum.php new file mode 100644 index 000000000..0c59a1ad9 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Entity/StateEnum.php @@ -0,0 +1,21 @@ + StateHistory::class])] +class StateHistory 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; + + public function __construct( + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, nullable: false, enumType: StateEnum::class)] + #[Serializer\Groups(['read'])] + private StateEnum $state, + #[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'), + ) { + $ticket->addStateHistory($this); + } + + public function getEndDate(): ?\DateTimeImmutable + { + return $this->endDate; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getState(): StateEnum + { + return $this->state; + } + + public function getStartDate(): \DateTimeImmutable + { + return $this->startDate; + } + + public function getTicket(): Ticket + { + return $this->ticket; + } + + public function setEndDate(?\DateTimeImmutable $endDate): void + { + $this->endDate = $endDate; + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php b/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php index 84b07eb17..128c27502 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/Ticket.php @@ -37,6 +37,7 @@ use Doctrine\ORM\Mapping as ORM; * - association between the ticket and motive: @see{MotiveHistory}; * - association between the ticket and addresses: @see{AddresseeHistory}; * - association between the ticket and input: @see{InputHistory}; + * - association between the ticket and state: @see{StateHistory}; */ #[ORM\Entity] #[ORM\Table(name: 'ticket', schema: 'chill_ticket')] @@ -83,6 +84,12 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface #[ORM\OneToMany(targetEntity: PersonHistory::class, mappedBy: 'ticket')] private Collection $personHistories; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: StateHistory::class, mappedBy: 'ticket', cascade: ['persist', 'remove'])] + private Collection $stateHistories; + public function __construct() { $this->addresseeHistory = new ArrayCollection(); @@ -90,6 +97,7 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface $this->motiveHistories = new ArrayCollection(); $this->personHistories = new ArrayCollection(); $this->inputHistories = new ArrayCollection(); + $this->stateHistories = new ArrayCollection(); } public function getId(): ?int @@ -225,4 +233,31 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface { return $this->addresseeHistory; } + + /** + * @internal use @see{StateHistory::__construct} instead + */ + public function addStateHistory(StateHistory $stateHistory): void + { + $this->stateHistories->add($stateHistory); + } + + public function getState(): ?StateEnum + { + foreach ($this->stateHistories as $stateHistory) { + if (null === $stateHistory->getEndDate()) { + return $stateHistory->getState(); + } + } + + return null; + } + + /** + * @return ReadableCollection + */ + public function getStateHistories(): ReadableCollection + { + return $this->stateHistories; + } } diff --git a/src/Bundle/ChillTicketBundle/src/migrations/Version20250603085035.php b/src/Bundle/ChillTicketBundle/src/migrations/Version20250603085035.php new file mode 100644 index 000000000..a99f550c5 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/migrations/Version20250603085035.php @@ -0,0 +1,89 @@ +addSql(<<<'SQL' + CREATE SEQUENCE chill_ticket.state_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1 + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE chill_ticket.state_history (id INT NOT NULL, ticket_id INT NOT NULL, + endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, state VARCHAR(255) NOT NULL, + startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, + createdBy_id INT DEFAULT NULL, PRIMARY KEY(id)) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_B1DCD379700047D2 ON chill_ticket.state_history (ticket_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_B1DCD3793174800F ON chill_ticket.state_history (createdBy_id) + SQL); + $this->addSql(<<<'SQL' + COMMENT ON COLUMN chill_ticket.state_history.endDate IS '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + COMMENT ON COLUMN chill_ticket.state_history.startDate IS '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + COMMENT ON COLUMN chill_ticket.state_history.createdAt IS '(DC2Type:datetime_immutable)' + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.state_history ADD CONSTRAINT FK_B1DCD379700047D2 FOREIGN KEY (ticket_id) + REFERENCES chill_ticket.ticket (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.state_history ADD CONSTRAINT FK_B1DCD3793174800F FOREIGN KEY (createdBy_id) + REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.state_history ADD CONSTRAINT ticket_state_not_overlaps + exclude using gist (ticket_id with =, tsrange(startdate, enddate) with &&) + deferrable initially deferred + SQL); + $this->addSql(<<<'SQL' + INSERT INTO chill_ticket.state_history (id, ticket_id, state, startDate, createdAt, createdBy_id) + SELECT nextval('chill_ticket.state_history_id_seq'), id, 'open', createdAt, createdAt, createdBy_id + FROM chill_ticket.ticket + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP SEQUENCE chill_ticket.state_history_id_seq CASCADE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.state_history DROP CONSTRAINT FK_B1DCD379700047D2 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.state_history DROP CONSTRAINT FK_B1DCD3793174800F + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE chill_ticket.state_history DROP CONSTRAINT ticket_state_not_overlaps + SQL); + $this->addSql(<<<'SQL' + DROP TABLE chill_ticket.state_history + SQL); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Entity/TicketTest.php b/src/Bundle/ChillTicketBundle/tests/Entity/TicketTest.php index b3a9f0b93..6ce8b53ea 100644 --- a/src/Bundle/ChillTicketBundle/tests/Entity/TicketTest.php +++ b/src/Bundle/ChillTicketBundle/tests/Entity/TicketTest.php @@ -18,6 +18,8 @@ use Chill\TicketBundle\Entity\AddresseeHistory; use Chill\TicketBundle\Entity\Motive; use Chill\TicketBundle\Entity\MotiveHistory; use Chill\TicketBundle\Entity\PersonHistory; +use Chill\TicketBundle\Entity\StateEnum; +use Chill\TicketBundle\Entity\StateHistory; use Chill\TicketBundle\Entity\Ticket; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -108,4 +110,29 @@ class TicketTest extends KernelTestCase // Verify that getExternalRef returns the updated value self::assertSame($newExternalRef, $ticket->getExternalRef()); } + + public function testGetState(): void + { + $ticket = new Ticket(); + $openState = new StateEnum(StateEnum::OPEN, ['en' => 'Open', 'fr' => 'Ouvert']); + + // Initially, the ticket has no state + self::assertNull($ticket->getState()); + + // Create a state history entry with the open state + $history = new StateHistory($openState, $ticket); + + // Verify that the ticket now has the open state + self::assertSame($openState, $ticket->getState()); + self::assertCount(1, $ticket->getStateHistories()); + + // Change the state to closed + $closedState = new StateEnum(StateEnum::CLOSED, ['en' => 'Closed', 'fr' => 'Fermé']); + $history->setEndDate(new \DateTimeImmutable()); + $history2 = new StateHistory($closedState, $ticket); + + // Verify that the ticket now has the closed state + self::assertCount(2, $ticket->getStateHistories()); + self::assertSame($closedState, $ticket->getState()); + } }