# معماری — تسک ۱۵: پرونده بیمار ## ساختار فایل‌ها ``` src/Patient/ ├── Controller/ │ └── PatientController.php ├── Entity/ │ ├── PatientRecord.php │ ├── PatientSession.php │ └── SessionService.php ← entity (سرویس‌های انجام‌شده در سشن) ├── Repository/ │ ├── PatientRecordRepository.php │ └── PatientSessionRepository.php └── Service/ └── PatientService.php ``` **فایل‌هایی که تغییر می‌کنند:** - `src/Appointment/Controller/AppointmentController.php` — متد `updateStatus()` باید `PatientService::autoCreateOnAppointmentConfirm()` صدا بزند ## Entity: PatientRecord ```php #[ORM\Entity] #[ORM\Table(name: 'patient_records')] #[ORM\UniqueConstraint(name: 'uniq_patient_record', columns: ['entity_type', 'entity_id', 'user_id'])] #[ORM\Index(columns: ['entity_type', 'entity_id'], name: 'idx_patient_records_entity')] class PatientRecord { #[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')] private ?int $id = null; #[ORM\Column(type: 'string', length: 36, unique: true)] private string $uuid; #[ORM\Column(type: 'string', length: 10)] private string $entityType; // 'doctor' | 'clinic' #[ORM\Column(type: 'integer')] private int $entityId; #[ORM\ManyToOne(targetEntity: \App\Auth\Entity\User::class)] #[ORM\JoinColumn(name: 'user_id', nullable: false, onDelete: 'RESTRICT')] private \App\Auth\Entity\User $user; // چه کسی پرونده را باز کرد #[ORM\Column(type: 'string', length: 15)] private string $createdByType; // 'doctor' | 'secretary' | 'system' #[ORM\Column(type: 'integer')] private int $createdById; #[ORM\Column(type: 'integer')] private int $createdAt; } ``` ## Entity: PatientSession ```php #[ORM\Entity] #[ORM\Table(name: 'patient_sessions')] #[ORM\Index(columns: ['record_id', 'created_at'], name: 'idx_patient_sessions_record')] class PatientSession { #[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: PatientRecord::class)] #[ORM\JoinColumn(name: 'record_id', nullable: false, onDelete: 'CASCADE')] private PatientRecord $record; #[ORM\ManyToOne(targetEntity: \App\Appointment\Entity\Appointment::class)] #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] private ?\App\Appointment\Entity\Appointment $appointment = null; // بیمه پایه — FK به categories bundle='insurance_type' #[ORM\Column(type: 'integer', nullable: true)] private ?int $insuranceBaseId = null; #[ORM\Column(type: 'integer', nullable: true)] private ?int $insuranceSupplementaryId = null; #[ORM\Column(type: 'integer')] private int $visitPriceRials = 0; #[ORM\Column(type: 'decimal', precision: 5, scale: 2)] private float $baseInsuranceDiscountPercent = 0; #[ORM\Column(type: 'decimal', precision: 5, scale: 2)] private float $supplementaryDiscountPercent = 0; #[ORM\Column(type: 'integer')] private int $servicesTotalRials = 0; #[ORM\Column(type: 'integer')] private int $finalPriceRials = 0; #[ORM\Column(type: 'string', length: 15)] private string $paymentMethod = 'pending'; // 'cash'|'card'|'insurance'|'pending' #[ORM\Column(type: 'text', nullable: true)] private ?string $notes = null; #[ORM\Column(type: 'integer')] private int $createdAt; #[ORM\Column(type: 'integer')] private int $updatedAt; } ``` ## Entity: SessionService (سرویس‌های انجام‌شده در سشن) ```php #[ORM\Entity] #[ORM\Table(name: 'session_services')] class SessionService { #[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(nullable: false, onDelete: 'CASCADE')] private PatientSession $session; #[ORM\ManyToOne(targetEntity: \App\ClinicService\Entity\ServiceItem::class)] #[ORM\JoinColumn(name: 'service_item_id', nullable: false, onDelete: 'RESTRICT')] private \App\ClinicService\Entity\ServiceItem $serviceItem; #[ORM\ManyToOne(targetEntity: \App\Staff\Entity\ClinicStaff::class)] #[ORM\JoinColumn(name: 'staff_id', nullable: true, onDelete: 'SET NULL')] private ?\App\Staff\Entity\ClinicStaff $staff = null; // کپی قیمت در زمان ثبت — تغییر قیمت سرویس بعداً اثر ندارد #[ORM\Column(type: 'integer')] private int $priceRials; #[ORM\Column(type: 'integer')] private int $createdAt; } ``` ## PatientService ```php class PatientService { public function calculateFinalPrice( int $visitPrice, float $baseDiscount, float $suppDiscount, array $serviceItems // [{price_rials}] ): array { $afterBase = $visitPrice * (1 - $baseDiscount / 100); $afterSupp = $afterBase * (1 - $suppDiscount / 100); $servicesTotal = array_sum(array_column($serviceItems, 'price_rials')); $finalPrice = (int) round($afterSupp) + $servicesTotal; return [ 'services_total_rials' => $servicesTotal, 'final_price_rials' => $finalPrice, ]; } public function autoCreateOnAppointmentConfirm(Appointment $appointment): void { [$entityType, $entityId] = $this->resolveEntityFromAppointment($appointment); if (!$this->subscriptionService->hasFeature($entityType, $entityId, 'patient_records')) { return; // پنل Free — ایجاد نشود } $userId = $appointment->getUser()->getId(); $record = $this->recordRepo->findOneBy([ 'entityType' => $entityType, 'entityId' => $entityId, 'user' => $userId, ]) ?? $this->createRecord($entityType, $entityId, $userId, 'system'); // سشن با appointment_id، visit_price=0 (تکمیل بعداً) $this->createSession($record, $appointment); } } ``` ## تغییر در AppointmentController ```php // src/Appointment/Controller/AppointmentController.php // متد updateStatus() — بعد از ذخیره status جدید: if ($newStatus === 'confirmed') { $this->patientService->autoCreateOnAppointmentConfirm($appointment); } ```