feat: Implement SMS sending functionality with KavehNegar and Rangineh providers
- Add SendSmsMessage class for encapsulating SMS message data. - Create KavehNegarProvider and RanginehProvider classes implementing SmsProviderInterface for sending SMS. - Implement SmsLogRepository and SmsTemplateRepository for managing SMS logs and templates. - Develop SendSmsHandler for handling SMS sending messages. - Create SmsService to manage SMS dispatching and logging. - Add UserProfileController for managing user profiles with CRUD operations. - Implement UserProfile entity and repository for user profile data management. - Update symfony.lock and bootstrap.php for project dependencies and environment setup.
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace App\Appointment\Controller;
|
||||
|
||||
use App\Appointment\Entity\Appointment;
|
||||
use App\Appointment\Repository\AppointmentRepository;
|
||||
use App\Appointment\Service\SlotCalculatorService;
|
||||
use App\Auth\Entity\User;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
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;
|
||||
|
||||
class AppointmentController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AppointmentRepository $appointmentRepo,
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
private readonly SlotCalculatorService $slotCalculator,
|
||||
) {}
|
||||
|
||||
// ── Public: available slots ───────────────────────────────────────────────
|
||||
|
||||
#[Route('/api/v1/appointment-slots', methods: ['GET'])]
|
||||
public function slots(Request $request): JsonResponse
|
||||
{
|
||||
$doctorUuid = trim($request->query->get('doctor_uuid', ''));
|
||||
$date = trim($request->query->get('date', ''));
|
||||
|
||||
$doctor = $this->doctorRepo->findByUuid($doctorUuid);
|
||||
if ($doctor === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
|
||||
}
|
||||
|
||||
if (empty($date) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'فرمت تاریخ نادرست است (Y-m-d)', 422, 'date');
|
||||
}
|
||||
|
||||
$slots = $this->slotCalculator->getAvailableSlots($doctor, $date);
|
||||
|
||||
return $this->success([
|
||||
'doctor_uuid' => $doctorUuid,
|
||||
'date' => $date,
|
||||
'slots' => $slots,
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Authenticated: book / manage ─────────────────────────────────────────
|
||||
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[Route('/api/v1/appointment', methods: ['POST'])]
|
||||
public function book(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$doctorUuid = trim($data['doctor_uuid'] ?? '');
|
||||
$slotStart = (int) ($data['slot_start'] ?? 0);
|
||||
$slotEnd = (int) ($data['slot_end'] ?? 0);
|
||||
|
||||
if (empty($doctorUuid) || $slotStart <= 0 || $slotEnd <= $slotStart) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'doctor_uuid، slot_start و slot_end الزامی است', 422);
|
||||
}
|
||||
|
||||
$doctor = $this->doctorRepo->findByUuid($doctorUuid);
|
||||
if ($doctor === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
|
||||
}
|
||||
|
||||
if ($this->appointmentRepo->isSlotTaken($doctor, $slotStart, $slotEnd)) {
|
||||
return $this->error(ErrorCodes::ERR_CONFLICT_001, 'این نوبت قبلاً رزرو شده است', 409);
|
||||
}
|
||||
|
||||
$appointment = new Appointment($doctor, $user, $slotStart, $slotEnd);
|
||||
if (isset($data['note'])) $appointment->setNote($data['note']);
|
||||
|
||||
$this->appointmentRepo->save($appointment);
|
||||
|
||||
return $this->success(['data' => $appointment->toArray()], 201);
|
||||
}
|
||||
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[Route('/api/v1/appointment/{uuid}', methods: ['GET'])]
|
||||
public function get(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$appointment = $this->appointmentRepo->findByUuid($uuid);
|
||||
if ($appointment === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'نوبت یافت نشد', 404);
|
||||
}
|
||||
|
||||
if (!$this->canView($appointment, $user)) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
|
||||
return $this->success(['data' => $appointment->toArray()]);
|
||||
}
|
||||
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[Route('/api/v1/appointments/doctor/{doctorUuid}', methods: ['GET'])]
|
||||
public function listByDoctor(string $doctorUuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$doctor = $this->doctorRepo->findByUuid($doctorUuid);
|
||||
if ($doctor === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
|
||||
}
|
||||
|
||||
if ($doctor->getUser()->getId() !== $user->getId() && !$user->hasRole('ROLE_ADMIN')) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
|
||||
$status = $request->query->get('status');
|
||||
$appointments = $this->appointmentRepo->findByDoctor($doctor, $status);
|
||||
|
||||
return $this->success(['data' => array_map(fn(Appointment $a) => $a->toArray(), $appointments)]);
|
||||
}
|
||||
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[Route('/api/v1/appointments/user', methods: ['GET'])]
|
||||
public function listByUser(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$status = $request->query->get('status');
|
||||
$appointments = $this->appointmentRepo->findByUser($user, $status);
|
||||
|
||||
return $this->success(['data' => array_map(fn(Appointment $a) => $a->toArray(), $appointments)]);
|
||||
}
|
||||
|
||||
private function canView(Appointment $a, User $user): bool
|
||||
{
|
||||
return $a->getUser()->getId() === $user->getId()
|
||||
|| $a->getDoctor()->getUser()->getId() === $user->getId()
|
||||
|| $user->hasRole('ROLE_ADMIN');
|
||||
}
|
||||
|
||||
private function canManage(Appointment $a, User $user): bool
|
||||
{
|
||||
return $a->getUser()->getId() === $user->getId()
|
||||
|| $a->getDoctor()->getUser()->getId() === $user->getId()
|
||||
|| $user->hasRole('ROLE_ADMIN');
|
||||
}
|
||||
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[Route('/api/v1/appointment/{uuid}/status', methods: ['PATCH'])]
|
||||
public function updateStatus(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$appointment = $this->appointmentRepo->findByUuid($uuid);
|
||||
if ($appointment === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'نوبت یافت نشد', 404);
|
||||
}
|
||||
|
||||
if (!$this->canManage($appointment, $user)) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$newStatus = trim($data['status'] ?? '');
|
||||
$version = (int) ($data['version'] ?? $appointment->getVersion());
|
||||
|
||||
if (!$appointment->canTransitionTo($newStatus)) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, sprintf(
|
||||
'انتقال از "%s" به "%s" مجاز نیست', $appointment->getStatus(), $newStatus
|
||||
), 422);
|
||||
}
|
||||
|
||||
$appointment->transitionTo($newStatus);
|
||||
|
||||
try {
|
||||
$this->appointmentRepo->saveWithLock($appointment, $version);
|
||||
} catch (OptimisticLockException) {
|
||||
return $this->error(ErrorCodes::ERR_CONFLICT_001, 'تداخل ویرایش همزمان. لطفاً دوباره تلاش کنید', 409);
|
||||
}
|
||||
|
||||
return $this->success(['data' => $appointment->toArray()]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user