Add CallerHistory entity to track ticket caller details and history

This commit is contained in:
Julien Fastré 2025-06-24 13:18:25 +02:00
parent 0566ab0910
commit 0cf922be64
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
5 changed files with 426 additions and 0 deletions

View File

@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* Represents the history of a caller associated with a ticket.
*
* This entity is used to track the changes in a ticket's caller over time.
* The caller can be either a Person or a ThirdParty.
* Implements the TrackCreationInterface for tracking entity lifecycle creation.
*/
#[ORM\Entity]
#[ORM\Table(name: 'caller_history', schema: 'chill_ticket')]
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['ticket_caller_history' => 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;
}
}

View File

@ -96,6 +96,12 @@ class Ticket implements TrackCreationInterface, TrackUpdateInterface
#[ORM\OneToMany(targetEntity: EmergencyStatusHistory::class, mappedBy: 'ticket', cascade: ['persist', 'remove'])]
private Collection $emergencyStatusHistories;
/**
* @var Collection<int, CallerHistory>
*/
#[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<int, CallerHistory>
*/
public function getCallerHistories(): ReadableCollection
{
return $this->callerHistories;
}
}

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Ticket;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250624105842 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add CallerHistory entity to associate a ticket with either a Person or a ThirdParty entity';
}
public function up(Schema $schema): void
{
$this->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);
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Tests\Entity;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\TicketBundle\Entity\CallerHistory;
use Chill\TicketBundle\Entity\Ticket;
use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @coversNothing
*/
class CallerHistoryTest extends TestCase
{
public function testConstructorWithPerson(): void
{
$ticket = new Ticket();
$callerHistory = new CallerHistory($person = new Person(), $ticket);
self::assertSame($ticket, $callerHistory->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());
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Tests\Entity;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\TicketBundle\Entity\CallerHistory;
use Chill\TicketBundle\Entity\Ticket;
use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @coversNothing
*/
class TicketCallerTest extends TestCase
{
public function testGetCaller(): void
{
$ticket = new Ticket();
// Initially, there should be no caller
self::assertNull($ticket->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());
}
}