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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user