feat: enhance staff management and payment gateway features
- Fix national code handling in staff creation and updates to support Persian digits. - Update ClinicStaff entity to allow longer national codes (up to 15 characters). - Implement support for clinic secretaries in SecretaryController, allowing creation without a doctor UUID. - Add a new endpoint to retrieve doctors associated with a clinic for secretary management. - Improve appointment management by ensuring doctors are selectable even when no appointments exist. - Extend PatientController to allow secretaries to create patient records if they have the appropriate permissions. - Introduce a PriceInput component for better price formatting in forms, supporting Persian digits. - Add a MockGateway for testing payment processes without real transactions. - Enhance SMS settings management with an approval flow for post-visit text messages, including new fields for pending text and status. - Update migrations to reflect changes in database schema for national codes and SMS settings.
This commit is contained in:
@@ -20,6 +20,19 @@ class SiteConfigController extends BaseController
|
||||
'support_phone',
|
||||
'max_cancel_hours_before',
|
||||
'appointment_reminder_hours',
|
||||
// payment gateways
|
||||
'payment_test_mode',
|
||||
'mellat_terminal_id',
|
||||
'mellat_username',
|
||||
'mellat_password',
|
||||
'sep_terminal_id',
|
||||
// sms provider
|
||||
'sms_provider',
|
||||
'kavenegar_api_key',
|
||||
'kavenegar_sender',
|
||||
'rangineh_api_key',
|
||||
'rangineh_sender',
|
||||
'sms_price_rials',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
|
||||
@@ -16,6 +16,19 @@ class SiteConfigRepository extends ServiceEntityRepository
|
||||
'support_phone' => '',
|
||||
'max_cancel_hours_before' => '24',
|
||||
'appointment_reminder_hours' => '2',
|
||||
// payment gateways
|
||||
'payment_test_mode' => '0',
|
||||
'mellat_terminal_id' => '',
|
||||
'mellat_username' => '',
|
||||
'mellat_password' => '',
|
||||
'sep_terminal_id' => '',
|
||||
// sms provider
|
||||
'sms_provider' => 'kavenegar',
|
||||
'kavenegar_api_key' => '',
|
||||
'kavenegar_sender' => '',
|
||||
'rangineh_api_key' => '',
|
||||
'rangineh_sender' => '',
|
||||
'sms_price_rials' => '500',
|
||||
];
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Auth\Repository\UserRepository;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\Patient\Entity\PatientRecord;
|
||||
use App\Secretary\Repository\DoctorSecretaryRepository;
|
||||
use App\Patient\Repository\PatientRecordRepository;
|
||||
use App\Patient\Repository\PatientSessionRepository;
|
||||
use App\Patient\Service\PatientService;
|
||||
@@ -24,13 +25,14 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
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,
|
||||
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,
|
||||
private readonly DoctorSecretaryRepository $secretaryRepo,
|
||||
) {}
|
||||
|
||||
#[Route('/api/v1/patients', methods: ['GET'])]
|
||||
@@ -170,6 +172,13 @@ class PatientController extends BaseController
|
||||
return $clinic !== null ? ['clinic', $clinic->getId()] : ['clinic', null];
|
||||
}
|
||||
|
||||
if ($user->hasRole('ROLE_SECRETARY')) {
|
||||
$secretary = $this->secretaryRepo->findActiveBySecretary($user);
|
||||
if ($secretary !== null && ($secretary->getPermissions()['appointments']['view'] ?? false)) {
|
||||
return ['doctor', $secretary->getDoctor()->getId()];
|
||||
}
|
||||
}
|
||||
|
||||
return ['unknown', null];
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ use App\Auth\Entity\User;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\Payment\Entity\Payment;
|
||||
use App\Config\Repository\SiteConfigRepository;
|
||||
use App\Payment\Gateway\MellatGateway;
|
||||
use App\Payment\Gateway\MockGateway;
|
||||
use App\Payment\Gateway\SepGateway;
|
||||
use App\Payment\Repository\PaymentRepository;
|
||||
use App\Payment\Service\CircuitBreakerService;
|
||||
@@ -38,11 +40,13 @@ class PaymentController extends BaseController
|
||||
private readonly AppointmentRepository $appointmentRepo,
|
||||
private readonly MellatGateway $mellat,
|
||||
private readonly SepGateway $sep,
|
||||
private readonly MockGateway $mock,
|
||||
private readonly CircuitBreakerService $circuitBreaker,
|
||||
private readonly SubscriptionService $subscriptionService,
|
||||
private readonly SmsWalletService $smsWalletService,
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
private readonly SiteConfigRepository $configRepo,
|
||||
private readonly string $appBaseUrl,
|
||||
private readonly string $allowedFrontendHosts = '',
|
||||
) {}
|
||||
@@ -520,6 +524,10 @@ class PaymentController extends BaseController
|
||||
|
||||
private function resolveGateway(string $name): \App\Payment\Gateway\PaymentGatewayInterface|null
|
||||
{
|
||||
if ($this->configRepo->get('payment_test_mode') === '1') {
|
||||
return $this->mock;
|
||||
}
|
||||
|
||||
return match ($name) {
|
||||
'mellat' => $this->mellat,
|
||||
'sep' => $this->sep,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Payment\Gateway;
|
||||
|
||||
use App\Config\Repository\SiteConfigRepository;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class MellatGateway implements PaymentGatewayInterface
|
||||
@@ -9,14 +10,20 @@ class MellatGateway implements PaymentGatewayInterface
|
||||
private const PAYMENT_URL = 'https://bpm.shaparak.ir/pgwchannel/startpay.mellat';
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly string $terminalId,
|
||||
private readonly string $username,
|
||||
private readonly string $password,
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly SiteConfigRepository $configRepo,
|
||||
private readonly string $terminalId = '',
|
||||
private readonly string $username = '',
|
||||
private readonly string $password = '',
|
||||
) {}
|
||||
|
||||
public function getName(): string { return 'mellat'; }
|
||||
|
||||
private function cfg(string $key, string $envFallback): string
|
||||
{
|
||||
return $this->configRepo->get($key) ?: $envFallback;
|
||||
}
|
||||
|
||||
public function initiate(int $amountRials, string $orderId, string $callbackUrl): PaymentInitResult
|
||||
{
|
||||
try {
|
||||
@@ -74,13 +81,17 @@ class MellatGateway implements PaymentGatewayInterface
|
||||
|
||||
private function buildRequestPayload(int $amount, string $orderId, string $callbackUrl): string
|
||||
{
|
||||
$terminalId = $this->cfg('mellat_terminal_id', $this->terminalId);
|
||||
$username = $this->cfg('mellat_username', $this->username);
|
||||
$password = $this->cfg('mellat_password', $this->password);
|
||||
|
||||
return <<<XML
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:int="http://interfaces.core.sw.bsl.com/westernmellat">
|
||||
<soapenv:Body>
|
||||
<int:bpPayRequest>
|
||||
<terminalId>{$this->terminalId}</terminalId>
|
||||
<userName>{$this->username}</userName>
|
||||
<userPassword>{$this->password}</userPassword>
|
||||
<terminalId>{$terminalId}</terminalId>
|
||||
<userName>{$username}</userName>
|
||||
<userPassword>{$password}</userPassword>
|
||||
<orderId>{$orderId}</orderId>
|
||||
<amount>{$amount}</amount>
|
||||
<localDate>{$this->date()}</localDate>
|
||||
@@ -96,13 +107,17 @@ XML;
|
||||
|
||||
private function buildVerifyPayload(string $refId): string
|
||||
{
|
||||
$terminalId = $this->cfg('mellat_terminal_id', $this->terminalId);
|
||||
$username = $this->cfg('mellat_username', $this->username);
|
||||
$password = $this->cfg('mellat_password', $this->password);
|
||||
|
||||
return <<<XML
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:int="http://interfaces.core.sw.bsl.com/westernmellat">
|
||||
<soapenv:Body>
|
||||
<int:bpVerifyRequest>
|
||||
<terminalId>{$this->terminalId}</terminalId>
|
||||
<userName>{$this->username}</userName>
|
||||
<userPassword>{$this->password}</userPassword>
|
||||
<terminalId>{$terminalId}</terminalId>
|
||||
<userName>{$username}</userName>
|
||||
<userPassword>{$password}</userPassword>
|
||||
<orderId>{$refId}</orderId>
|
||||
<saleOrderId>{$refId}</saleOrderId>
|
||||
<saleReferenceId>{$refId}</saleReferenceId>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Payment\Gateway;
|
||||
|
||||
class MockGateway implements PaymentGatewayInterface
|
||||
{
|
||||
public function getName(): string { return 'mock'; }
|
||||
|
||||
public function initiate(int $amountRials, string $orderId, string $callbackUrl): PaymentInitResult
|
||||
{
|
||||
$redirectUrl = $callbackUrl . '&mock=1&ResCode=0&RefId=MOCK-' . $orderId;
|
||||
return new PaymentInitResult(true, redirectUrl: $redirectUrl, token: 'MOCK-' . $orderId);
|
||||
}
|
||||
|
||||
public function verify(array $callbackData): PaymentVerifyResult
|
||||
{
|
||||
$mock = $callbackData['mock'] ?? '0';
|
||||
$resCode = $callbackData['ResCode'] ?? ($callbackData['State'] ?? '');
|
||||
|
||||
if ($mock !== '1' && $mock !== 1) {
|
||||
return new PaymentVerifyResult(false, errorMessage: 'mock callback مجاز نیست');
|
||||
}
|
||||
|
||||
$refId = $callbackData['RefId'] ?? $callbackData['order_id'] ?? 'MOCK-REF';
|
||||
return new PaymentVerifyResult(true, referenceId: $refId);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Payment\Gateway;
|
||||
|
||||
use App\Config\Repository\SiteConfigRepository;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class SepGateway implements PaymentGatewayInterface
|
||||
@@ -10,19 +11,25 @@ class SepGateway implements PaymentGatewayInterface
|
||||
private const PAYMENT_URL = 'https://sep.shaparak.ir/OnlinePG/OnlinePG';
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly string $terminalId,
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly SiteConfigRepository $configRepo,
|
||||
private readonly string $terminalId = '',
|
||||
) {}
|
||||
|
||||
public function getName(): string { return 'sep'; }
|
||||
|
||||
private function cfg(string $key, string $envFallback): string
|
||||
{
|
||||
return $this->configRepo->get($key) ?: $envFallback;
|
||||
}
|
||||
|
||||
public function initiate(int $amountRials, string $orderId, string $callbackUrl): PaymentInitResult
|
||||
{
|
||||
try {
|
||||
$response = $this->httpClient->request('POST', self::TOKEN_URL, [
|
||||
'json' => [
|
||||
'action' => 'token',
|
||||
'TerminalId' => $this->terminalId,
|
||||
'TerminalId' => $this->cfg('sep_terminal_id', $this->terminalId),
|
||||
'Amount' => $amountRials,
|
||||
'ResNum' => $orderId,
|
||||
'RedirectUrl' => $callbackUrl,
|
||||
@@ -57,7 +64,7 @@ class SepGateway implements PaymentGatewayInterface
|
||||
$response = $this->httpClient->request('POST', self::TOKEN_URL, [
|
||||
'json' => [
|
||||
'action' => 'verify',
|
||||
'TerminalId' => $this->terminalId,
|
||||
'TerminalId' => $this->cfg('sep_terminal_id', $this->terminalId),
|
||||
'RefNum' => $refNum,
|
||||
],
|
||||
'timeout' => 10,
|
||||
|
||||
@@ -4,7 +4,9 @@ namespace App\Secretary\Controller;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\Auth\Repository\UserRepository;
|
||||
use App\Clinic\Entity\Clinic;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\Secretary\Entity\DoctorSecretary;
|
||||
use App\Secretary\Repository\DoctorSecretaryRepository;
|
||||
@@ -46,8 +48,7 @@ class SecretaryController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
|
||||
}
|
||||
|
||||
// Only the doctor owner or admin can create secretary
|
||||
if ($doctor->getUser()->getId() !== $currentUser->getId() && !$currentUser->hasRole('ROLE_ADMIN')) {
|
||||
if (!$this->canManageDoctor($doctor, $currentUser)) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
|
||||
@@ -164,7 +165,7 @@ class SecretaryController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
|
||||
}
|
||||
|
||||
if ($doctor->getUser()->getId() !== $currentUser->getId() && !$currentUser->hasRole('ROLE_ADMIN')) {
|
||||
if (!$this->canManageDoctor($doctor, $currentUser)) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
|
||||
@@ -176,9 +177,49 @@ class SecretaryController extends BaseController
|
||||
return $this->success(['data' => $secretaries]);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/secretaries/clinic/{clinicUuid}', methods: ['GET'])]
|
||||
public function listByClinic(string $clinicUuid, #[CurrentUser] User $currentUser): JsonResponse
|
||||
{
|
||||
$clinic = $this->clinicRepo->findByUuid($clinicUuid);
|
||||
if ($clinic === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'کلینیک یافت نشد', 404);
|
||||
}
|
||||
|
||||
if ($clinic->getUser()->getId() !== $currentUser->getId() && !$currentUser->hasRole('ROLE_ADMIN')) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
|
||||
$secretaries = array_map(
|
||||
fn(DoctorSecretary $s) => $s->toArray(),
|
||||
$this->secretaryRepo->findByClinic($clinic)
|
||||
);
|
||||
|
||||
return $this->success(['data' => $secretaries]);
|
||||
}
|
||||
|
||||
private function canManage(DoctorSecretary $secretary, User $user): bool
|
||||
{
|
||||
return $secretary->getDoctor()->getUser()->getId() === $user->getId()
|
||||
|| $user->hasRole('ROLE_ADMIN');
|
||||
return $this->canManageDoctor($secretary->getDoctor(), $user);
|
||||
}
|
||||
|
||||
private function canManageDoctor(Doctor $doctor, User $user): bool
|
||||
{
|
||||
if ($user->hasRole('ROLE_ADMIN')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($doctor->getUser()->getId() === $user->getId()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// clinic owner can manage secretaries of its own doctors
|
||||
if ($user->hasRole('ROLE_CLINIC')) {
|
||||
$clinic = $this->clinicRepo->findByUser($user);
|
||||
if ($clinic !== null && $this->secretaryRepo->isDoctorInClinic($doctor, $clinic)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,20 +97,14 @@ class DoctorSecretary
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'uuid' => $this->uuid,
|
||||
'user' => [
|
||||
'uuid' => $this->secretary->getUuid(),
|
||||
'realname' => $this->secretary->getRealName(),
|
||||
'mobile' => $this->secretary->getMobileNumber(),
|
||||
'picture' => null,
|
||||
],
|
||||
'doctor' => [
|
||||
'uuid' => $this->doctor->getUuid(),
|
||||
'name' => $this->doctor->getName(),
|
||||
],
|
||||
'active' => $this->active,
|
||||
'permissions' => $this->getPermissions(),
|
||||
'created_at' => $this->createdAt,
|
||||
'uuid' => $this->uuid,
|
||||
'user_name' => $this->secretary->getRealName(),
|
||||
'mobile_number' => $this->secretary->getMobileNumber(),
|
||||
'doctor_name' => $this->doctor->getName(),
|
||||
'doctor_uuid' => $this->doctor->getUuid(),
|
||||
'is_active' => $this->active,
|
||||
'permissions' => $this->getPermissions(),
|
||||
'created_at' => $this->createdAt,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Secretary\Repository;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\Clinic\Entity\Clinic;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Secretary\Entity\DoctorSecretary;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
@@ -37,6 +38,28 @@ class DoctorSecretaryRepository extends ServiceEntityRepository
|
||||
return $this->findBy(['doctor' => $doctor], ['createdAt' => 'DESC']);
|
||||
}
|
||||
|
||||
public function isDoctorInClinic(Doctor $doctor, Clinic $clinic): bool
|
||||
{
|
||||
return $clinic->getDoctors()->contains($doctor);
|
||||
}
|
||||
|
||||
/** @return DoctorSecretary[] — all secretaries across all doctors of a clinic */
|
||||
public function findByClinic(Clinic $clinic): array
|
||||
{
|
||||
$doctorIds = $clinic->getDoctors()->map(fn(Doctor $d) => $d->getId())->toArray();
|
||||
if (empty($doctorIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->createQueryBuilder('s')
|
||||
->join('s.doctor', 'd')
|
||||
->where('d.id IN (:ids)')
|
||||
->setParameter('ids', $doctorIds)
|
||||
->orderBy('s.createdAt', 'DESC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function findActiveBySecretary(User $user): ?DoctorSecretary
|
||||
{
|
||||
return $this->findOneBy(['secretary' => $user, 'active' => true]);
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Config\Repository\SiteConfigRepository;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\Payment\Entity\Payment;
|
||||
use App\Payment\Gateway\MellatGateway;
|
||||
use App\Payment\Gateway\MockGateway;
|
||||
use App\Payment\Gateway\SepGateway;
|
||||
use App\Payment\Repository\PaymentRepository;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
@@ -35,6 +36,7 @@ class SmsWalletController extends BaseController
|
||||
private readonly SiteConfigRepository $configRepo,
|
||||
private readonly MellatGateway $mellat,
|
||||
private readonly SepGateway $sep,
|
||||
private readonly MockGateway $mock,
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
private readonly string $appBaseUrl,
|
||||
@@ -75,11 +77,15 @@ class SmsWalletController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_PAYMENT_002, 'مبلغ شارژ نامعتبر است', 422);
|
||||
}
|
||||
|
||||
$gateway = match ($gatewayName) {
|
||||
'mellat' => $this->mellat,
|
||||
'sep' => $this->sep,
|
||||
default => null,
|
||||
};
|
||||
if ($this->configRepo->get('payment_test_mode') === '1') {
|
||||
$gateway = $this->mock;
|
||||
} else {
|
||||
$gateway = match ($gatewayName) {
|
||||
'mellat' => $this->mellat,
|
||||
'sep' => $this->sep,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
if ($gateway === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'درگاه پرداخت نامعتبر است', 422);
|
||||
@@ -172,13 +178,67 @@ class SmsWalletController extends BaseController
|
||||
if (isset($data['reminder_enabled'])) { $settings->setReminderEnabled((bool) $data['reminder_enabled']); }
|
||||
if (isset($data['reminder_hours_before'])) { $settings->setReminderHoursBefore((int) $data['reminder_hours_before']); }
|
||||
if (isset($data['post_visit_enabled'])) { $settings->setPostVisitEnabled((bool) $data['post_visit_enabled']); }
|
||||
if (array_key_exists('post_visit_text', $data)) { $settings->setPostVisitText($data['post_visit_text']); }
|
||||
if (array_key_exists('post_visit_text', $data) && $data['post_visit_text'] !== null) {
|
||||
$text = trim((string) $data['post_visit_text']);
|
||||
if ($text !== '') {
|
||||
$settings->submitPostVisitText($text);
|
||||
}
|
||||
}
|
||||
|
||||
$this->settingsRepo->save($settings);
|
||||
|
||||
return $this->success($settings->toArray());
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/sms/settings/review', methods: ['GET'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function adminReviewList(): JsonResponse
|
||||
{
|
||||
$pending = $this->settingsRepo->createQueryBuilder('s')
|
||||
->where('s.postVisitTextStatus = :status')
|
||||
->setParameter('status', SmsSettings::TEXT_STATUS_PENDING)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
return $this->success(['data' => array_map(fn(SmsSettings $s) => $s->toArray(), $pending)]);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/sms/settings/{id}/approve', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function adminApprove(int $id): JsonResponse
|
||||
{
|
||||
$settings = $this->settingsRepo->find($id);
|
||||
if ($settings === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'تنظیمات یافت نشد', 404);
|
||||
}
|
||||
|
||||
$settings->approvePostVisitText();
|
||||
$this->settingsRepo->save($settings);
|
||||
|
||||
return $this->success($settings->toArray());
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/sms/settings/{id}/reject', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function adminReject(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$settings = $this->settingsRepo->find($id);
|
||||
if ($settings === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'تنظیمات یافت نشد', 404);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$reason = trim($data['reason'] ?? '');
|
||||
if ($reason === '') {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'دلیل رد الزامی است', 422);
|
||||
}
|
||||
|
||||
$settings->rejectPostVisitText($reason);
|
||||
$this->settingsRepo->save($settings);
|
||||
|
||||
return $this->success($settings->toArray());
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/sms/wallet-report', methods: ['GET'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function adminReport(Request $request): JsonResponse
|
||||
|
||||
@@ -33,9 +33,23 @@ class SmsSettings
|
||||
#[ORM\Column(name: 'post_visit_text', type: 'text', nullable: true)]
|
||||
private ?string $postVisitText = null;
|
||||
|
||||
#[ORM\Column(name: 'post_visit_text_pending', type: 'text', nullable: true)]
|
||||
private ?string $postVisitTextPending = null;
|
||||
|
||||
#[ORM\Column(name: 'post_visit_text_status', type: 'string', length: 20)]
|
||||
private string $postVisitTextStatus = 'none';
|
||||
|
||||
#[ORM\Column(name: 'post_visit_text_reject_reason', type: 'text', nullable: true)]
|
||||
private ?string $postVisitTextRejectReason = null;
|
||||
|
||||
#[ORM\Column(name: 'updated_at', type: 'integer')]
|
||||
private int $updatedAt;
|
||||
|
||||
public const TEXT_STATUS_NONE = 'none';
|
||||
public const TEXT_STATUS_PENDING = 'pending';
|
||||
public const TEXT_STATUS_APPROVED = 'approved';
|
||||
public const TEXT_STATUS_REJECTED = 'rejected';
|
||||
|
||||
public function __construct(string $entityType, int $entityId)
|
||||
{
|
||||
$this->entityType = $entityType;
|
||||
@@ -51,21 +65,56 @@ class SmsSettings
|
||||
public function isPostVisitEnabled(): bool { return $this->postVisitEnabled; }
|
||||
public function getPostVisitText(): ?string { return $this->postVisitText; }
|
||||
|
||||
public function getPostVisitTextPending(): ?string { return $this->postVisitTextPending; }
|
||||
public function getPostVisitTextStatus(): string { return $this->postVisitTextStatus; }
|
||||
public function getPostVisitTextRejectReason(): ?string { return $this->postVisitTextRejectReason; }
|
||||
|
||||
public function setReminderEnabled(bool $v): self { $this->reminderEnabled = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setReminderHoursBefore(int $v): self { $this->reminderHoursBefore = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setPostVisitEnabled(bool $v): self { $this->postVisitEnabled = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setPostVisitText(?string $v): self { $this->postVisitText = $v; $this->updatedAt = time(); return $this; }
|
||||
|
||||
public function submitPostVisitText(string $text): self
|
||||
{
|
||||
$this->postVisitTextPending = $text;
|
||||
$this->postVisitTextStatus = self::TEXT_STATUS_PENDING;
|
||||
$this->postVisitTextRejectReason = null;
|
||||
$this->updatedAt = time();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function approvePostVisitText(): self
|
||||
{
|
||||
if ($this->postVisitTextPending !== null) {
|
||||
$this->postVisitText = $this->postVisitTextPending;
|
||||
}
|
||||
$this->postVisitTextPending = null;
|
||||
$this->postVisitTextStatus = self::TEXT_STATUS_APPROVED;
|
||||
$this->updatedAt = time();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function rejectPostVisitText(string $reason): self
|
||||
{
|
||||
$this->postVisitTextStatus = self::TEXT_STATUS_REJECTED;
|
||||
$this->postVisitTextRejectReason = $reason;
|
||||
$this->updatedAt = time();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'entity_type' => $this->entityType,
|
||||
'entity_id' => $this->entityId,
|
||||
'reminder_enabled' => $this->reminderEnabled,
|
||||
'reminder_hours_before' => $this->reminderHoursBefore,
|
||||
'post_visit_enabled' => $this->postVisitEnabled,
|
||||
'post_visit_text' => $this->postVisitText,
|
||||
'updated_at' => $this->updatedAt,
|
||||
'entity_type' => $this->entityType,
|
||||
'entity_id' => $this->entityId,
|
||||
'reminder_enabled' => $this->reminderEnabled,
|
||||
'reminder_hours_before' => $this->reminderHoursBefore,
|
||||
'post_visit_enabled' => $this->postVisitEnabled,
|
||||
'post_visit_text' => $this->postVisitText,
|
||||
'post_visit_text_pending' => $this->postVisitTextPending,
|
||||
'post_visit_text_status' => $this->postVisitTextStatus,
|
||||
'post_visit_text_reject_reason' => $this->postVisitTextRejectReason,
|
||||
'updated_at' => $this->updatedAt,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Sms\Provider;
|
||||
|
||||
use App\Config\Repository\SiteConfigRepository;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class KavehNegarProvider implements SmsProviderInterface
|
||||
@@ -9,22 +10,26 @@ class KavehNegarProvider implements SmsProviderInterface
|
||||
private const BASE = 'https://api.kavenegar.com/v1';
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly string $apiKey,
|
||||
private readonly string $sender,
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly SiteConfigRepository $configRepo,
|
||||
private readonly string $apiKey = '',
|
||||
private readonly string $sender = '',
|
||||
) {}
|
||||
|
||||
private function key(): string { return $this->configRepo->get('kavenegar_api_key') ?: $this->apiKey; }
|
||||
private function sender(): string { return $this->configRepo->get('kavenegar_sender') ?: $this->sender; }
|
||||
|
||||
public function getName(): string { return 'kavenegar'; }
|
||||
|
||||
public function send(string $mobile, string $message): bool
|
||||
{
|
||||
try {
|
||||
$resp = $this->httpClient->request('POST',
|
||||
self::BASE . '/' . $this->apiKey . '/sms/send.json', [
|
||||
self::BASE . '/' . $this->key() . '/sms/send.json', [
|
||||
'body' => http_build_query([
|
||||
'receptor' => $mobile,
|
||||
'message' => $message,
|
||||
'sender' => $this->sender,
|
||||
'sender' => $this->sender(),
|
||||
]),
|
||||
'timeout' => 10,
|
||||
]
|
||||
@@ -44,7 +49,7 @@ class KavehNegarProvider implements SmsProviderInterface
|
||||
$params['token' . ($i > 0 ? $i + 1 : '')] = $v;
|
||||
}
|
||||
$resp = $this->httpClient->request('POST',
|
||||
self::BASE . '/' . $this->apiKey . '/verify/lookup.json', [
|
||||
self::BASE . '/' . $this->key() . '/verify/lookup.json', [
|
||||
'body' => http_build_query($params),
|
||||
'timeout' => 10,
|
||||
]
|
||||
|
||||
@@ -59,7 +59,7 @@ class StaffController extends BaseController
|
||||
$staff->setPhone($data['phone'] ?? null);
|
||||
$staff->setJobTitle($data['job_title'] ?? null);
|
||||
$staff->setAddress($data['address'] ?? null);
|
||||
$staff->setNationalCode($data['national_code'] ?? null);
|
||||
$staff->setNationalCode($this->toLatinDigits($data['national_code'] ?? null));
|
||||
|
||||
$this->staffRepo->save($staff);
|
||||
|
||||
@@ -86,7 +86,7 @@ class StaffController extends BaseController
|
||||
if (array_key_exists('phone', $data)) { $staff->setPhone($data['phone']); }
|
||||
if (array_key_exists('job_title', $data)) { $staff->setJobTitle($data['job_title']); }
|
||||
if (array_key_exists('address', $data)) { $staff->setAddress($data['address']); }
|
||||
if (array_key_exists('national_code', $data)){ $staff->setNationalCode($data['national_code']); }
|
||||
if (array_key_exists('national_code', $data)){ $staff->setNationalCode($this->toLatinDigits($data['national_code'])); }
|
||||
|
||||
$this->staffRepo->save($staff);
|
||||
|
||||
@@ -137,4 +137,17 @@ class StaffController extends BaseController
|
||||
&& $staff->getEntityType() === $entityType
|
||||
&& $staff->getEntityId() === $entityId;
|
||||
}
|
||||
|
||||
private function toLatinDigits(?string $str): ?string
|
||||
{
|
||||
if ($str === null) {
|
||||
return null;
|
||||
}
|
||||
return strtr($str, [
|
||||
'۰' => '0', '۱' => '1', '۲' => '2', '۳' => '3', '۴' => '4',
|
||||
'۵' => '5', '۶' => '6', '۷' => '7', '۸' => '8', '۹' => '9',
|
||||
'٠' => '0', '١' => '1', '٢' => '2', '٣' => '3', '٤' => '4',
|
||||
'٥' => '5', '٦' => '6', '٧' => '7', '٨' => '8', '٩' => '9',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class ClinicStaff
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
private ?string $address = null;
|
||||
|
||||
#[ORM\Column(name: 'national_code', type: 'string', length: 10, nullable: true)]
|
||||
#[ORM\Column(name: 'national_code', type: 'string', length: 15, nullable: true)]
|
||||
private ?string $nationalCode = null;
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
|
||||
Reference in New Issue
Block a user