feat(patient): add SessionAuditLog entity for financial/service change trail

New SessionAuditLog (mirrors AppointmentEvent): session FK, field, operation
(create/update/delete), old_value/new_value, actor id+name, note, created_at.
Repository saves and lists a session's history (newest first, array hydration).
Migration creates the table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-17 15:06:25 +03:30
co-authored by Claude Fable 5
parent 0d943d8fc5
commit d8c8ba0df7
3 changed files with 149 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260717113519 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add session_audit_logs table (financial/service change audit trail)';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE session_audit_logs (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(36) NOT NULL, field VARCHAR(40) NOT NULL, operation VARCHAR(10) NOT NULL, old_value LONGTEXT DEFAULT NULL, new_value LONGTEXT DEFAULT NULL, actor_user_id INT DEFAULT NULL, actor_name VARCHAR(191) DEFAULT NULL, note VARCHAR(191) DEFAULT NULL, created_at INT NOT NULL, session_id INT NOT NULL, UNIQUE INDEX UNIQ_36B92303D17F50A6 (uuid), INDEX IDX_36B92303613FECDF (session_id), INDEX idx_session_audit_session (session_id, created_at), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('ALTER TABLE session_audit_logs ADD CONSTRAINT FK_36B92303613FECDF FOREIGN KEY (session_id) REFERENCES patient_sessions (id) ON DELETE CASCADE');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE session_audit_logs DROP FOREIGN KEY FK_36B92303613FECDF');
$this->addSql('DROP TABLE session_audit_logs');
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
namespace App\Patient\Entity;
use App\Patient\Repository\SessionAuditLogRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
/**
* تاریخچه‌ی تغییرات مالی/خدماتی یک مراجعه (Audit Log): چه کسی، چه چیزی، کِی و
* مقدار قبل/بعد را تغییر داد. برای شفافیت و قابلیت پیگیری کامل پرونده.
*/
#[ORM\Entity(repositoryClass: SessionAuditLogRepository::class)]
#[ORM\Table(name: 'session_audit_logs')]
#[ORM\Index(columns: ['session_id', 'created_at'], name: 'idx_session_audit_session')]
class SessionAuditLog
{
public const OP_CREATE = 'create';
public const OP_UPDATE = 'update';
public const OP_DELETE = 'delete';
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 36, unique: true)]
private string $uuid;
#[ORM\ManyToOne(targetEntity: PatientSession::class)]
#[ORM\JoinColumn(name: 'session_id', nullable: false, onDelete: 'CASCADE')]
private PatientSession $session;
/** فیلد تغییرکرده: visit_price_rials | services | consumables | payment | discount | ... */
#[ORM\Column(type: 'string', length: 40)]
private string $field;
#[ORM\Column(type: 'string', length: 10)]
private string $operation;
#[ORM\Column(name: 'old_value', type: 'text', nullable: true)]
private ?string $oldValue = null;
#[ORM\Column(name: 'new_value', type: 'text', nullable: true)]
private ?string $newValue = null;
#[ORM\Column(name: 'actor_user_id', type: 'integer', nullable: true)]
private ?int $actorUserId = null;
#[ORM\Column(name: 'actor_name', type: 'string', length: 191, nullable: true)]
private ?string $actorName = null;
#[ORM\Column(type: 'string', length: 191, nullable: true)]
private ?string $note = null;
#[ORM\Column(name: 'created_at', type: 'integer')]
private int $createdAt;
public function __construct(PatientSession $session, string $field, string $operation)
{
$this->uuid = Uuid::v4()->toRfc4122();
$this->session = $session;
$this->field = $field;
$this->operation = $operation;
$this->createdAt = time();
}
public function setActor(?int $userId, ?string $name): self { $this->actorUserId = $userId; $this->actorName = $name; return $this; }
public function setValues(?string $old, ?string $new): self { $this->oldValue = $old; $this->newValue = $new; return $this; }
public function setNote(?string $note): self { $this->note = $note; return $this; }
public function getUuid(): string { return $this->uuid; }
public function toArray(): array
{
return [
'uuid' => $this->uuid,
'field' => $this->field,
'operation' => $this->operation,
'old_value' => $this->oldValue,
'new_value' => $this->newValue,
'actor_name' => $this->actorName,
'note' => $this->note,
'created_at' => $this->createdAt,
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Patient\Repository;
use App\Patient\Entity\SessionAuditLog;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class SessionAuditLogRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry) { parent::__construct($registry, SessionAuditLog::class); }
public function save(SessionAuditLog $e, bool $flush = true): void
{
$this->getEntityManager()->persist($e);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/** تاریخچه‌ی یک مراجعه (جدید → قدیم)، آرایه‌ای. */
public function findBySessionUuid(string $sessionUuid): array
{
return $this->createQueryBuilder('l')
->select('l.field AS field', 'l.operation AS operation', 'l.oldValue AS old_value', 'l.newValue AS new_value', 'l.actorName AS actor_name', 'l.note AS note', 'l.createdAt AS created_at')
->join('l.session', 's')
->where('s.uuid = :uuid')->setParameter('uuid', $sessionUuid)
->orderBy('l.createdAt', 'DESC')->addOrderBy('l.id', 'DESC')
->getQuery()->getArrayResult();
}
}