feat: implement staff management and subscription system

- Added StaffController for managing clinic staff, including listing, creating, updating, and toggling staff status.
- Created ClinicStaff entity and repository for staff data handling.
- Developed SubscriptionController to manage subscription plans and periods, including trial subscriptions.
- Introduced SubscriptionPlan, SubscriptionPeriod, and ClinicSubscription entities for subscription management.
- Implemented SubscriptionService for handling subscription logic, including trial activation and subscription creation from payments.
- Added necessary repositories for subscription entities to facilitate data access and manipulation.
This commit is contained in:
hamed
2026-06-14 22:10:28 +03:30
parent dcd631f503
commit b0244f28f5
53 changed files with 4434 additions and 40 deletions
@@ -0,0 +1,193 @@
<?php
namespace App\Patient\Controller;
use App\Auth\Entity\User;
use App\Auth\Repository\UserRepository;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Repository\DoctorRepository;
use App\Patient\Entity\PatientRecord;
use App\Patient\Repository\PatientRecordRepository;
use App\Patient\Repository\PatientSessionRepository;
use App\Patient\Service\PatientService;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use App\Shared\Exception\AppException;
use App\Subscription\Service\SubscriptionService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class PatientController extends BaseController
{
public function __construct(
private readonly PatientRecordRepository $recordRepo,
private readonly PatientSessionRepository $sessionRepo,
private readonly PatientService $patientService,
private readonly SubscriptionService $subscriptionService,
private readonly UserRepository $userRepo,
private readonly DoctorRepository $doctorRepo,
private readonly ClinicRepository $clinicRepo,
) {}
#[Route('/api/v1/patients', methods: ['GET'])]
public function list(Request $request, #[CurrentUser] User $user): JsonResponse
{
[$entityType, $entityId] = $this->resolveEntity($user);
$this->assertPatientGate($entityType, $entityId);
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(50, max(10, (int) $request->query->get('limit', 20)));
$search = $request->query->get('search') ?: null;
$records = $this->recordRepo->findByEntity($entityType, $entityId, $page, $limit, $search);
$total = $this->recordRepo->countByEntity($entityType, $entityId, $search);
return $this->paginated(
array_map(fn(PatientRecord $r) => $r->toArray(), $records),
$total,
$page,
$limit
);
}
#[Route('/api/v1/patient', methods: ['POST'])]
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
{
[$entityType, $entityId] = $this->resolveEntity($user);
$this->assertPatientGate($entityType, $entityId);
$data = json_decode($request->getContent(), true) ?? [];
$userUuid = trim($data['user_uuid'] ?? '');
if ($userUuid === '') {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'user_uuid الزامی است', 422);
}
$patient = $this->userRepo->findByUuid($userUuid);
if ($patient === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'کاربر یافت نشد', 404);
}
$existing = $this->recordRepo->findByEntityAndUser($entityType, $entityId, $patient);
if ($existing !== null) {
return $this->success($existing->toArray());
}
$record = new PatientRecord($entityType, $entityId, $patient, $user->hasRole('ROLE_DOCTOR') ? 'doctor' : 'clinic', $entityId);
$this->recordRepo->save($record);
return $this->success($record->toArray(), 201);
}
#[Route('/api/v1/patient/{uuid}', methods: ['GET'])]
public function show(string $uuid, #[CurrentUser] User $user): JsonResponse
{
[$entityType, $entityId] = $this->resolveEntity($user);
$this->assertPatientGate($entityType, $entityId);
$record = $this->recordRepo->findByUuid($uuid);
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId)) {
return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
}
return $this->success($record->toArray());
}
#[Route('/api/v1/patient/{uuid}/sessions', methods: ['GET'])]
public function sessions(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
{
[$entityType, $entityId] = $this->resolveEntity($user);
$this->assertPatientGate($entityType, $entityId);
$record = $this->recordRepo->findByUuid($uuid);
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId)) {
return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
}
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(50, max(10, (int) $request->query->get('limit', 20)));
$sessions = $this->sessionRepo->findByRecord($record, $page, $limit);
$total = $this->sessionRepo->countByRecord($record);
return $this->paginated(
array_map(fn($s) => $s->toArray(), $sessions),
$total,
$page,
$limit
);
}
#[Route('/api/v1/patient/{uuid}/session', methods: ['POST'])]
public function createSession(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
{
[$entityType, $entityId] = $this->resolveEntity($user);
$this->assertPatientGate($entityType, $entityId);
$record = $this->recordRepo->findByUuid($uuid);
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId)) {
return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
}
$data = json_decode($request->getContent(), true) ?? [];
$session = $this->patientService->createSession($record, $data, $entityType, $entityId);
return $this->success($session->toArray(), 201);
}
#[Route('/api/v1/session/{uuid}', methods: ['PATCH'])]
public function updateSession(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
{
[$entityType, $entityId] = $this->resolveEntity($user);
$session = $this->sessionRepo->findByUuid($uuid);
if ($session === null || !$this->ownsRecord($session->getRecord(), $entityType, $entityId)) {
return $this->error(ErrorCodes::ERR_SESSION_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_SESSION_NOT_FOUND), 404);
}
$data = json_decode($request->getContent(), true) ?? [];
if (isset($data['notes'])) { $session->setNotes($data['notes']); }
if (isset($data['payment_method'])) { $session->setPaymentMethod($data['payment_method']); }
$this->sessionRepo->save($session);
return $this->success($session->toArray());
}
private function resolveEntity(User $user): array
{
if ($user->hasRole('ROLE_DOCTOR')) {
$doctor = $this->doctorRepo->findByUser($user);
return $doctor !== null ? ['doctor', $doctor->getId()] : ['doctor', null];
}
if ($user->hasRole('ROLE_CLINIC')) {
$clinic = $this->clinicRepo->findByUser($user);
return $clinic !== null ? ['clinic', $clinic->getId()] : ['clinic', null];
}
return ['unknown', null];
}
private function assertPatientGate(string $entityType, ?int $entityId): void
{
if ($entityId === null) {
throw new AppException(ErrorCodes::ERR_FORBIDDEN_001, null, 403);
}
if (!$this->subscriptionService->hasFeature($entityType, $entityId, 'patient_records')) {
throw new AppException(ErrorCodes::ERR_SUBSCRIPTION_REQUIRED, null, 403);
}
}
private function ownsRecord($record, string $entityType, ?int $entityId): bool
{
return $entityId !== null
&& $record->getEntityType() === $entityType
&& $record->getEntityId() === $entityId;
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace App\Patient\Entity;
use App\Auth\Entity\User;
use App\Patient\Repository\PatientRecordRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: PatientRecordRepository::class)]
#[ORM\Table(name: 'patient_records')]
#[ORM\UniqueConstraint(name: 'uniq_patient_record', columns: ['entity_type', 'entity_id', 'user_id'])]
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(name: 'entity_type', type: 'string', length: 10)]
private string $entityType;
#[ORM\Column(name: 'entity_id', type: 'integer')]
private int $entityId;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'RESTRICT')]
private User $user;
#[ORM\Column(name: 'created_by_type', type: 'string', length: 15)]
private string $createdByType;
#[ORM\Column(name: 'created_by_id', type: 'integer')]
private int $createdById;
#[ORM\Column(name: 'created_at', type: 'integer')]
private int $createdAt;
#[ORM\OneToMany(targetEntity: PatientSession::class, mappedBy: 'record', cascade: ['remove'])]
private Collection $sessions;
public function __construct(string $entityType, int $entityId, User $user, string $createdByType, int $createdById)
{
$this->uuid = Uuid::v4()->toRfc4122();
$this->entityType = $entityType;
$this->entityId = $entityId;
$this->user = $user;
$this->createdByType = $createdByType;
$this->createdById = $createdById;
$this->createdAt = time();
$this->sessions = new ArrayCollection();
}
public function getId(): ?int { return $this->id; }
public function getUuid(): string { return $this->uuid; }
public function getEntityType(): string { return $this->entityType; }
public function getEntityId(): int { return $this->entityId; }
public function getUser(): User { return $this->user; }
public function getCreatedByType(): string { return $this->createdByType; }
public function getCreatedById(): int { return $this->createdById; }
public function getCreatedAt(): int { return $this->createdAt; }
public function toArray(): array
{
return [
'uuid' => $this->uuid,
'entity_type' => $this->entityType,
'entity_id' => $this->entityId,
'user_uuid' => $this->user->getUuid(),
'user_name' => $this->user->getRealName(),
'user_mobile' => $this->user->getMobileNumber(),
'created_by_type' => $this->createdByType,
'created_at' => $this->createdAt,
];
}
}
+121
View File
@@ -0,0 +1,121 @@
<?php
namespace App\Patient\Entity;
use App\Appointment\Entity\Appointment;
use App\Patient\Repository\PatientSessionRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: PatientSessionRepository::class)]
#[ORM\Table(name: 'patient_sessions')]
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, inversedBy: 'sessions')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private PatientRecord $record;
#[ORM\ManyToOne(targetEntity: Appointment::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?Appointment $appointment = null;
#[ORM\Column(name: 'insurance_base_id', type: 'integer', nullable: true)]
private ?int $insuranceBaseId = null;
#[ORM\Column(name: 'insurance_supplementary_id', type: 'integer', nullable: true)]
private ?int $insuranceSupplementaryId = null;
#[ORM\Column(name: 'visit_price_rials', type: 'integer')]
private int $visitPriceRials = 0;
#[ORM\Column(name: 'base_insurance_discount_percent', type: 'decimal', precision: 5, scale: 2)]
private string $baseInsuranceDiscountPercent = '0.00';
#[ORM\Column(name: 'supplementary_discount_percent', type: 'decimal', precision: 5, scale: 2)]
private string $supplementaryDiscountPercent = '0.00';
#[ORM\Column(name: 'services_total_rials', type: 'integer')]
private int $servicesTotalRials = 0;
#[ORM\Column(name: 'final_price_rials', type: 'integer')]
private int $finalPriceRials = 0;
#[ORM\Column(name: 'payment_method', type: 'string', length: 15)]
private string $paymentMethod = 'pending';
#[ORM\Column(type: 'text', nullable: true)]
private ?string $notes = null;
#[ORM\Column(name: 'created_at', type: 'integer')]
private int $createdAt;
#[ORM\Column(name: 'updated_at', type: 'integer')]
private int $updatedAt;
#[ORM\OneToMany(targetEntity: SessionService::class, mappedBy: 'session', cascade: ['remove'])]
private Collection $services;
public function __construct(PatientRecord $record, ?Appointment $appointment = null)
{
$this->uuid = Uuid::v4()->toRfc4122();
$this->record = $record;
$this->appointment = $appointment;
$this->createdAt = time();
$this->updatedAt = time();
$this->services = new ArrayCollection();
}
public function getId(): ?int { return $this->id; }
public function getUuid(): string { return $this->uuid; }
public function getRecord(): PatientRecord { return $this->record; }
public function getAppointment(): ?Appointment { return $this->appointment; }
public function getVisitPriceRials(): int { return $this->visitPriceRials; }
public function getBaseInsuranceDiscountPercent(): float { return (float) $this->baseInsuranceDiscountPercent; }
public function getSupplementaryDiscountPercent(): float { return (float) $this->supplementaryDiscountPercent; }
public function getServicesTotalRials(): int { return $this->servicesTotalRials; }
public function getFinalPriceRials(): int { return $this->finalPriceRials; }
public function getPaymentMethod(): string { return $this->paymentMethod; }
public function getNotes(): ?string { return $this->notes; }
public function getCreatedAt(): int { return $this->createdAt; }
public function getUpdatedAt(): int { return $this->updatedAt; }
public function setInsuranceBaseId(?int $id): self { $this->insuranceBaseId = $id; $this->updatedAt = time(); return $this; }
public function setInsuranceSupplementaryId(?int $id): self { $this->insuranceSupplementaryId = $id; $this->updatedAt = time(); return $this; }
public function setVisitPriceRials(int $v): self { $this->visitPriceRials = $v; $this->updatedAt = time(); return $this; }
public function setBaseInsuranceDiscountPercent(float $v): self { $this->baseInsuranceDiscountPercent = (string) $v; $this->updatedAt = time(); return $this; }
public function setSupplementaryDiscountPercent(float $v): self { $this->supplementaryDiscountPercent = (string) $v; $this->updatedAt = time(); return $this; }
public function setServicesTotalRials(int $v): self { $this->servicesTotalRials = $v; $this->updatedAt = time(); return $this; }
public function setFinalPriceRials(int $v): self { $this->finalPriceRials = $v; $this->updatedAt = time(); return $this; }
public function setPaymentMethod(string $v): self { $this->paymentMethod = $v; $this->updatedAt = time(); return $this; }
public function setNotes(?string $v): self { $this->notes = $v; $this->updatedAt = time(); return $this; }
public function toArray(): array
{
return [
'uuid' => $this->uuid,
'record_uuid' => $this->record->getUuid(),
'appointment_uuid' => $this->appointment?->getUuid(),
'insurance_base_id' => $this->insuranceBaseId,
'insurance_supplementary_id' => $this->insuranceSupplementaryId,
'visit_price_rials' => $this->visitPriceRials,
'base_insurance_discount_percent' => (float) $this->baseInsuranceDiscountPercent,
'supplementary_discount_percent' => (float) $this->supplementaryDiscountPercent,
'services_total_rials' => $this->servicesTotalRials,
'final_price_rials' => $this->finalPriceRials,
'payment_method' => $this->paymentMethod,
'notes' => $this->notes,
'created_at' => $this->createdAt,
'updated_at' => $this->updatedAt,
];
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace App\Patient\Entity;
use App\ClinicService\Entity\ServiceItem;
use App\Patient\Repository\SessionServiceRepository;
use App\Staff\Entity\ClinicStaff;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: SessionServiceRepository::class)]
#[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, inversedBy: 'services')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private PatientSession $session;
#[ORM\ManyToOne(targetEntity: ServiceItem::class)]
#[ORM\JoinColumn(name: 'service_item_id', nullable: false, onDelete: 'RESTRICT')]
private ServiceItem $serviceItem;
#[ORM\ManyToOne(targetEntity: ClinicStaff::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?ClinicStaff $staff = null;
#[ORM\Column(name: 'price_rials', type: 'integer')]
private int $priceRials;
#[ORM\Column(name: 'created_at', type: 'integer')]
private int $createdAt;
public function __construct(PatientSession $session, ServiceItem $serviceItem, ?ClinicStaff $staff = null)
{
$this->uuid = Uuid::v4()->toRfc4122();
$this->session = $session;
$this->serviceItem = $serviceItem;
$this->staff = $staff;
$this->priceRials = $serviceItem->getPriceRials();
$this->createdAt = time();
}
public function getId(): ?int { return $this->id; }
public function getUuid(): string { return $this->uuid; }
public function getSession(): PatientSession { return $this->session; }
public function getServiceItem(): ServiceItem { return $this->serviceItem; }
public function getStaff(): ?ClinicStaff { return $this->staff; }
public function getPriceRials(): int { return $this->priceRials; }
public function getCreatedAt(): int { return $this->createdAt; }
public function toArray(): array
{
return [
'uuid' => $this->uuid,
'service_item_uuid' => $this->serviceItem->getUuid(),
'service_name' => $this->serviceItem->getName(),
'staff_uuid' => $this->staff?->getUuid(),
'staff_name' => $this->staff?->getFullName(),
'price_rials' => $this->priceRials,
'created_at' => $this->createdAt,
];
}
}
@@ -0,0 +1,90 @@
<?php
namespace App\Patient\Repository;
use App\Auth\Entity\User;
use App\Patient\Entity\PatientRecord;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class PatientRecordRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, PatientRecord::class);
}
public function findByUuid(string $uuid): ?PatientRecord
{
return $this->findOneBy(['uuid' => $uuid]);
}
public function findByEntityAndUser(string $entityType, int $entityId, User $user): ?PatientRecord
{
return $this->findOneBy([
'entityType' => $entityType,
'entityId' => $entityId,
'user' => $user,
]);
}
public function findByEntity(string $entityType, int $entityId, int $page = 1, int $limit = 20, ?string $search = null): array
{
$qb = $this->createQueryBuilder('r')
->join('r.user', 'u')
->where('r.entityType = :type')
->andWhere('r.entityId = :id')
->setParameter('type', $entityType)
->setParameter('id', $entityId)
->orderBy('r.id', 'DESC')
->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit);
if ($search !== null && $search !== '') {
$qb->andWhere('u.realName LIKE :search OR u.mobileNumber LIKE :search')
->setParameter('search', '%' . $search . '%');
}
return $qb->getQuery()->getResult();
}
public function countByEntity(string $entityType, int $entityId, ?string $search = null): int
{
$qb = $this->createQueryBuilder('r')
->select('COUNT(r.id)')
->join('r.user', 'u')
->where('r.entityType = :type')
->andWhere('r.entityId = :id')
->setParameter('type', $entityType)
->setParameter('id', $entityId);
if ($search !== null && $search !== '') {
$qb->andWhere('u.realName LIKE :search OR u.mobileNumber LIKE :search')
->setParameter('search', '%' . $search . '%');
}
return (int) $qb->getQuery()->getSingleScalarResult();
}
public function countUnique(string $entityType, int $entityId, int $from, int $to): int
{
return (int) $this->createQueryBuilder('r')
->select('COUNT(DISTINCT r.user)')
->join('r.sessions', 's')
->where('r.entityType = :type')
->andWhere('r.entityId = :id')
->andWhere('s.createdAt BETWEEN :from AND :to')
->setParameter('type', $entityType)
->setParameter('id', $entityId)
->setParameter('from', $from)
->setParameter('to', $to)
->getQuery()
->getSingleScalarResult();
}
public function save(PatientRecord $record): void
{
$this->getEntityManager()->persist($record);
$this->getEntityManager()->flush();
}
}
@@ -0,0 +1,67 @@
<?php
namespace App\Patient\Repository;
use App\Patient\Entity\PatientRecord;
use App\Patient\Entity\PatientSession;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class PatientSessionRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, PatientSession::class);
}
public function findByUuid(string $uuid): ?PatientSession
{
return $this->findOneBy(['uuid' => $uuid]);
}
public function findByRecord(PatientRecord $record, int $page = 1, int $limit = 20): array
{
return $this->createQueryBuilder('s')
->where('s.record = :record')
->setParameter('record', $record)
->orderBy('s.id', 'DESC')
->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit)
->getQuery()
->getResult();
}
public function countByRecord(PatientRecord $record): int
{
return (int) $this->createQueryBuilder('s')
->select('COUNT(s.id)')
->where('s.record = :record')
->setParameter('record', $record)
->getQuery()
->getSingleScalarResult();
}
public function sumRevenue(string $entityType, int $entityId, int $from, int $to): int
{
$result = $this->createQueryBuilder('s')
->select('SUM(s.finalPriceRials)')
->join('s.record', 'r')
->where('r.entityType = :type')
->andWhere('r.entityId = :id')
->andWhere('s.createdAt BETWEEN :from AND :to')
->setParameter('type', $entityType)
->setParameter('id', $entityId)
->setParameter('from', $from)
->setParameter('to', $to)
->getQuery()
->getSingleScalarResult();
return (int) ($result ?? 0);
}
public function save(PatientSession $session): void
{
$this->getEntityManager()->persist($session);
$this->getEntityManager()->flush();
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Patient\Repository;
use App\Patient\Entity\SessionService;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class SessionServiceRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, SessionService::class);
}
public function save(SessionService $service): void
{
$this->getEntityManager()->persist($service);
$this->getEntityManager()->flush();
}
}
+117
View File
@@ -0,0 +1,117 @@
<?php
namespace App\Patient\Service;
use App\Appointment\Entity\Appointment;
use App\Auth\Repository\UserRepository;
use App\ClinicService\Repository\ServiceItemRepository;
use App\Patient\Entity\PatientRecord;
use App\Patient\Entity\PatientSession;
use App\Patient\Entity\SessionService;
use App\Patient\Repository\PatientRecordRepository;
use App\Patient\Repository\PatientSessionRepository;
use App\Patient\Repository\SessionServiceRepository;
use App\Staff\Repository\ClinicStaffRepository;
use App\Subscription\Service\SubscriptionService;
class PatientService
{
public function __construct(
private readonly PatientRecordRepository $recordRepo,
private readonly PatientSessionRepository $sessionRepo,
private readonly SessionServiceRepository $sessionServiceRepo,
private readonly ServiceItemRepository $serviceItemRepo,
private readonly ClinicStaffRepository $staffRepo,
private readonly UserRepository $userRepo,
private readonly SubscriptionService $subscriptionService,
) {}
public function calculateFinalPrice(int $visitPrice, float $baseDiscount, float $suppDiscount, array $serviceItems): array
{
$afterBase = $visitPrice * (1 - $baseDiscount / 100);
$afterSupp = $afterBase * (1 - $suppDiscount / 100);
$servicesTotal = array_sum(array_column($serviceItems, 'price_rials'));
return [
'services_total_rials' => (int) $servicesTotal,
'final_price_rials' => (int) round($afterSupp) + (int) $servicesTotal,
];
}
public function autoCreateOnAppointmentConfirm(Appointment $appointment): void
{
$doctor = $appointment->getDoctor();
$entityType = 'doctor';
$entityId = $doctor->getId();
if (!$this->subscriptionService->hasFeature($entityType, $entityId, 'patient_records')) {
return;
}
$patient = $appointment->getUser();
$record = $this->recordRepo->findByEntityAndUser($entityType, $entityId, $patient);
if ($record === null) {
$record = new PatientRecord($entityType, $entityId, $patient, 'system', $doctor->getId());
$this->recordRepo->save($record);
}
$session = new PatientSession($record, $appointment);
$this->sessionRepo->save($session);
}
public function createSession(
PatientRecord $record,
array $data,
string $entityType,
int $entityId
): PatientSession {
$session = new PatientSession($record);
if (!empty($data['appointment_uuid'])) {
// appointment را از بیرون resolve می‌کنند و session را ست می‌کنند
}
$session->setInsuranceBaseId(isset($data['insurance_base_id']) ? (int) $data['insurance_base_id'] : null);
$session->setInsuranceSupplementaryId(isset($data['insurance_supplementary_id']) ? (int) $data['insurance_supplementary_id'] : null);
$session->setVisitPriceRials((int) ($data['visit_price_rials'] ?? 0));
$session->setBaseInsuranceDiscountPercent((float) ($data['base_insurance_discount_percent'] ?? 0));
$session->setSupplementaryDiscountPercent((float) ($data['supplementary_discount_percent'] ?? 0));
$session->setPaymentMethod($data['payment_method'] ?? 'pending');
$session->setNotes($data['notes'] ?? null);
// جمع‌آوری service items
$serviceItemsData = [];
foreach (($data['services'] ?? []) as $svc) {
$item = $this->serviceItemRepo->findByUuid($svc['service_item_uuid'] ?? '');
if ($item !== null) {
$serviceItemsData[] = ['price_rials' => $item->getPriceRials()];
}
}
$priceCalc = $this->calculateFinalPrice(
$session->getVisitPriceRials(),
$session->getBaseInsuranceDiscountPercent(),
$session->getSupplementaryDiscountPercent(),
$serviceItemsData
);
$session->setServicesTotalRials($priceCalc['services_total_rials']);
$session->setFinalPriceRials($priceCalc['final_price_rials']);
$this->sessionRepo->save($session);
// ثبت session services
foreach (($data['services'] ?? []) as $svc) {
$item = $this->serviceItemRepo->findByUuid($svc['service_item_uuid'] ?? '');
if ($item === null) {
continue;
}
$staff = !empty($svc['staff_uuid']) ? $this->staffRepo->findByUuid($svc['staff_uuid']) : null;
$ss = new SessionService($session, $item, $staff);
$this->sessionServiceRepo->save($ss);
}
return $session;
}
}