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:
hamed
2026-06-15 11:03:56 +03:30
parent 55f646e2d4
commit 5cdcec23a9
32 changed files with 1487 additions and 128 deletions
@@ -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,
+25 -10
View File
@@ -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>
+27
View File
@@ -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);
}
}
+11 -4
View File
@@ -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,