feat(payment): implement PaymentManager for handling payment logic and callbacks
- Refactor PaymentController to delegate payment processing to PaymentManager. - Add findByOrderIdForUpdate method in PaymentRepository for pessimistic locking. - Create PaymentLog entity and repository for auditing payment actions. - Implement startGatewayHandoff and processCallback methods in PaymentManager. - Introduce transaction handling and logging for payment verification. - Update payment flow to ensure idempotency and prevent race conditions. - Enhance security by logging sensitive actions without exposing credentials. - Update database schema with migration for payment_logs table. - Document changes in payment flow architecture.
This commit is contained in:
@@ -5,20 +5,13 @@ namespace App\Payment\Controller;
|
||||
use App\Appointment\Entity\Appointment;
|
||||
use App\Appointment\Repository\AppointmentRepository;
|
||||
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\Gateway\GatewayFactory;
|
||||
use App\Payment\Repository\PaymentRepository;
|
||||
use App\Payment\Service\CircuitBreakerService;
|
||||
use App\Payment\Service\PaymentManager;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use App\Sms\Service\SmsService;
|
||||
use App\Sms\Service\SmsWalletService;
|
||||
use App\Subscription\Service\SubscriptionService;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
@@ -39,19 +32,9 @@ class PaymentController extends BaseController
|
||||
public function __construct(
|
||||
private readonly PaymentRepository $paymentRepo,
|
||||
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 SmsService $smsService,
|
||||
private readonly \App\Sms\Service\SmsTextResolver $smsText,
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
private readonly GatewayFactory $gateways,
|
||||
private readonly PaymentManager $paymentManager,
|
||||
private readonly SiteConfigRepository $configRepo,
|
||||
private readonly \App\Settlement\Service\CommissionService $commissionService,
|
||||
private readonly \App\Representation\Service\JalaliDateService $jalali,
|
||||
private readonly string $appBaseUrl,
|
||||
private readonly string $allowedFrontendHosts = '',
|
||||
) {}
|
||||
@@ -84,7 +67,7 @@ class PaymentController extends BaseController
|
||||
property: 'data',
|
||||
properties: [
|
||||
new OA\Property(property: 'payment_uuid', type: 'string', format: 'uuid'),
|
||||
new OA\Property(property: 'redirect_url', type: 'string', format: 'uri'),
|
||||
new OA\Property(property: 'pay_url', type: 'string', format: 'uri', description: 'مرورگر به این آدرس بکاند هدایت شود؛ بکاند به درگاه منتقل میکند'),
|
||||
new OA\Property(property: 'order_id', type: 'string'),
|
||||
],
|
||||
type: 'object'
|
||||
@@ -92,63 +75,8 @@ class PaymentController extends BaseController
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
description: 'Unauthorized',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'code', type: 'string'),
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
description: 'Validation error',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'code', type: 'string'),
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 503,
|
||||
description: 'Gateway unavailable',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'code', type: 'string'),
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||
new OA\Response(response: 422, description: 'Validation error'),
|
||||
]
|
||||
)]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
@@ -180,7 +108,7 @@ class PaymentController extends BaseController
|
||||
|
||||
// اعتبارسنجی اولیهٔ درگاه (فعال/معتبر بودن)؛ ارتباط با بانک اینجا انجام
|
||||
// نمیشود — در GET /payment/pay هنگام انتقال مرورگر به درگاه انجام میگیرد.
|
||||
if ($this->resolveGateway($gatewayName) === null) {
|
||||
if ($this->gateways->resolve($gatewayName) === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'درگاه پرداخت نامعتبر یا غیرفعال است', 422, 'gateway');
|
||||
}
|
||||
|
||||
@@ -224,35 +152,12 @@ class PaymentController extends BaseController
|
||||
return $this->redirectToFrontend($payment, $payment->getStatus() === Payment::STATUS_SUCCESS);
|
||||
}
|
||||
|
||||
$gatewayName = $payment->getGateway();
|
||||
$gateway = $this->resolveGateway($gatewayName);
|
||||
$testMode = $this->configRepo->get('payment_test_mode') === '1';
|
||||
|
||||
if ($gateway === null || (!$testMode && $this->circuitBreaker->isOpen($gatewayName))) {
|
||||
$payment->setStatus(Payment::STATUS_FAILED);
|
||||
$this->paymentRepo->save($payment);
|
||||
// ارتباط با بانک + init در سرویس انجام میشود؛ کنترلر فقط انتقال HTTP را میسازد.
|
||||
$result = $this->paymentManager->startGatewayHandoff($payment);
|
||||
if ($result === false) {
|
||||
return $this->redirectToFrontend($payment, false);
|
||||
}
|
||||
|
||||
// ارتباط با بانک (init) از سمت بکاند انجام میشود.
|
||||
$callbackUrl = $this->appBaseUrl . '/api/v1/payment/callback/' . $gatewayName . '?order_id=' . $payment->getOrderId();
|
||||
$result = $gateway->initiate($payment->getAmountRials(), $payment->getOrderId(), $callbackUrl);
|
||||
|
||||
if (!$result->success) {
|
||||
if (!$testMode) {
|
||||
$this->circuitBreaker->recordFailure($gatewayName);
|
||||
}
|
||||
$payment->setStatus(Payment::STATUS_FAILED);
|
||||
$this->paymentRepo->save($payment);
|
||||
return $this->redirectToFrontend($payment, false);
|
||||
}
|
||||
|
||||
if (!$testMode) {
|
||||
$this->circuitBreaker->recordSuccess($gatewayName);
|
||||
}
|
||||
$payment->setGatewayToken($result->token);
|
||||
$this->paymentRepo->save($payment);
|
||||
|
||||
// انتقال مرورگر به درگاه: 302 برای درگاه GET (سپ/mock) یا فرم auto-submit POST (ملت).
|
||||
if ($result->redirectMethod === 'POST') {
|
||||
return $this->autoSubmitForm(strtok($result->redirectUrl, '?'), $result->redirectParams);
|
||||
@@ -313,75 +218,21 @@ HTML;
|
||||
#[Route('/api/v1/payment/callback/{gateway}', methods: ['POST', 'GET'])]
|
||||
public function callback(string $gateway, Request $request): \Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
$clientIp = $request->getClientIp() ?? '';
|
||||
$isTestMode = $this->configRepo->get('payment_test_mode') === '1';
|
||||
if (!$isTestMode && !$this->isAllowedCallbackIp($clientIp)) {
|
||||
$clientIp = $request->getClientIp() ?? '';
|
||||
if (!$this->gateways->isTestMode() && !$this->isAllowedCallbackIp($clientIp)) {
|
||||
return new JsonResponse(['success' => false, 'message' => 'دسترسی ممنوع'], 403);
|
||||
}
|
||||
|
||||
$callbackData = array_merge($request->query->all(), $request->request->all());
|
||||
$orderId = $callbackData['order_id'] ?? $callbackData['ResNum'] ?? '';
|
||||
|
||||
$payment = $this->paymentRepo->findByOrderId($orderId);
|
||||
// verify امن (transaction + قفل + idempotent + post-action + log) در سرویس.
|
||||
$payment = $this->paymentManager->processCallback($gateway, $callbackData, $clientIp, $orderId);
|
||||
if ($payment === null) {
|
||||
return new JsonResponse(['success' => false, 'message' => 'payment not found'], 404);
|
||||
}
|
||||
|
||||
$payment->setCallbackIp($clientIp);
|
||||
|
||||
$gw = $this->resolveGateway($gateway);
|
||||
$result = $gw?->verify($callbackData) ?? null;
|
||||
|
||||
if ($result === null || !$result->success) {
|
||||
$canceled = $result !== null && $result->canceled;
|
||||
$payment->setStatus($canceled ? Payment::STATUS_CANCELED : Payment::STATUS_FAILED);
|
||||
$this->paymentRepo->save($payment);
|
||||
if (!$canceled) {
|
||||
$this->circuitBreaker->recordFailure($gateway);
|
||||
}
|
||||
|
||||
return $this->redirectToFrontend($payment, false);
|
||||
}
|
||||
|
||||
$this->circuitBreaker->recordSuccess($gateway);
|
||||
|
||||
// Gateway-confirmed amount must match the amount we charged. Gateways that
|
||||
// report the settled amount (SEP: AffectiveAmount) let us catch an
|
||||
// underpayment / RefNum-replay; gateways that don't report it bind the
|
||||
// amount server-side to the original request, so amountRials is 0 here.
|
||||
if ($result->amountRials > 0 && $result->amountRials !== $payment->getAmountRials()) {
|
||||
$payment->setStatus(Payment::STATUS_FAILED);
|
||||
$this->paymentRepo->save($payment);
|
||||
|
||||
return $this->redirectToFrontend($payment, false);
|
||||
}
|
||||
|
||||
// A gateway reference identifies exactly one settled transaction. If it
|
||||
// already belongs to another payment, this is a replay — reject it. The
|
||||
// unique DB index on reference_id is the hard backstop behind this check.
|
||||
if ($result->referenceId !== '') {
|
||||
$owner = $this->paymentRepo->findByReferenceId($result->referenceId);
|
||||
if ($owner !== null && $owner->getId() !== $payment->getId()) {
|
||||
$payment->setStatus(Payment::STATUS_FAILED);
|
||||
$this->paymentRepo->save($payment);
|
||||
|
||||
return $this->redirectToFrontend($payment, false);
|
||||
}
|
||||
}
|
||||
|
||||
$payment->setStatus(Payment::STATUS_SUCCESS);
|
||||
$payment->setReferenceId($result->referenceId);
|
||||
$this->paymentRepo->save($payment);
|
||||
|
||||
if ($payment->getType() === Payment::TYPE_SUBSCRIPTION) {
|
||||
$this->handleSubscriptionActivation($payment);
|
||||
} elseif ($payment->getType() === Payment::TYPE_SMS_WALLET) {
|
||||
$this->handleSmsWalletCharge($payment);
|
||||
} elseif ($payment->getType() === Payment::TYPE_APPOINTMENT) {
|
||||
$this->handleAppointmentConfirmation($payment);
|
||||
}
|
||||
|
||||
return $this->redirectToFrontend($payment, true);
|
||||
return $this->redirectToFrontend($payment, $payment->getStatus() === Payment::STATUS_SUCCESS);
|
||||
}
|
||||
|
||||
// ── Subscription Payment ──────────────────────────────────────────────────
|
||||
@@ -412,7 +263,7 @@ HTML;
|
||||
property: 'data',
|
||||
properties: [
|
||||
new OA\Property(property: 'payment_uuid', type: 'string', format: 'uuid'),
|
||||
new OA\Property(property: 'redirect_url', type: 'string', format: 'uri'),
|
||||
new OA\Property(property: 'pay_url', type: 'string', format: 'uri'),
|
||||
new OA\Property(property: 'order_id', type: 'string'),
|
||||
],
|
||||
type: 'object'
|
||||
@@ -420,63 +271,8 @@ HTML;
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
description: 'Unauthorized',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'code', type: 'string'),
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
description: 'Validation error',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'code', type: 'string'),
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 503,
|
||||
description: 'Gateway unavailable',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'code', type: 'string'),
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||
new OA\Response(response: 422, description: 'Validation error'),
|
||||
]
|
||||
)]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
@@ -496,15 +292,10 @@ HTML;
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'آدرس بازگشت مجاز نیست', 422, 'frontend_address');
|
||||
}
|
||||
|
||||
$gateway = $this->resolveGateway($gatewayName);
|
||||
if ($gateway === null) {
|
||||
if ($this->gateways->resolve($gatewayName) === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'درگاه پرداخت نامعتبر یا غیرفعال است', 422, 'gateway');
|
||||
}
|
||||
|
||||
if ($this->circuitBreaker->isOpen($gatewayName)) {
|
||||
return $this->error(ErrorCodes::ERR_PAYMENT_001, ErrorCodes::message(ErrorCodes::ERR_PAYMENT_001), 503);
|
||||
}
|
||||
|
||||
$periodUuid = trim($data['period_uuid'] ?? '');
|
||||
$payment = new Payment($user, $amountRials, $gatewayName, Payment::TYPE_SUBSCRIPTION, $frontendAddress);
|
||||
if ($periodUuid !== '') {
|
||||
@@ -512,21 +303,10 @@ HTML;
|
||||
}
|
||||
$this->paymentRepo->save($payment);
|
||||
|
||||
$callbackUrl = $this->appBaseUrl . '/api/v1/subscription-payment/callback/' . $gatewayName . '?order_id=' . $payment->getOrderId();
|
||||
$result = $gateway->initiate($amountRials, $payment->getOrderId(), $callbackUrl);
|
||||
|
||||
if (!$result->success) {
|
||||
$this->circuitBreaker->recordFailure($gatewayName);
|
||||
return $this->error(ErrorCodes::ERR_PAYMENT_001, $result->errorMessage, 503);
|
||||
}
|
||||
|
||||
$this->circuitBreaker->recordSuccess($gatewayName);
|
||||
$payment->setGatewayToken($result->token);
|
||||
$this->paymentRepo->save($payment);
|
||||
|
||||
// مثل appointment: ارتباط با بانک اینجا نیست؛ در GET /payment/pay انجام میشود.
|
||||
return $this->success([
|
||||
'payment_uuid' => $payment->getUuid(),
|
||||
'redirect_url' => $result->redirectUrl,
|
||||
'pay_url' => $this->appBaseUrl . '/api/v1/payment/pay/' . $payment->getOrderId(),
|
||||
'order_id' => $payment->getOrderId(),
|
||||
]);
|
||||
}
|
||||
@@ -588,18 +368,12 @@ HTML;
|
||||
new OA\Property(
|
||||
property: 'data',
|
||||
properties: [
|
||||
new OA\Property(
|
||||
property: 'data',
|
||||
properties: [
|
||||
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||
new OA\Property(property: 'status', type: 'string'),
|
||||
new OA\Property(property: 'amount_rials', type: 'integer'),
|
||||
new OA\Property(property: 'gateway', type: 'string'),
|
||||
new OA\Property(property: 'reference_id', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
],
|
||||
type: 'object'
|
||||
),
|
||||
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||
new OA\Property(property: 'status', type: 'string'),
|
||||
new OA\Property(property: 'amount_rials', type: 'integer'),
|
||||
new OA\Property(property: 'gateway', type: 'string'),
|
||||
new OA\Property(property: 'reference_id', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'created_at', type: 'integer'),
|
||||
],
|
||||
type: 'object'
|
||||
),
|
||||
@@ -615,36 +389,13 @@ HTML;
|
||||
#[Route('/api/v1/payment/config', methods: ['GET'])]
|
||||
public function config(): JsonResponse
|
||||
{
|
||||
$testMode = $this->configRepo->get('payment_test_mode') === '1';
|
||||
|
||||
return $this->success([
|
||||
'test_mode' => $testMode,
|
||||
'test_mode' => $this->gateways->isTestMode(),
|
||||
'appointment_fee_rials' => (int) ($this->configRepo->get('appointment_fee_rials') ?: 0),
|
||||
'gateways' => $this->activeGateways($testMode),
|
||||
'gateways' => $this->gateways->activeGateways(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* درگاههای قابلانتخاب: در حالت تست فقط درگاه آزمایشی، در غیر این صورت هر درگاهی که اعتبارنامهاش ست شده.
|
||||
* @return array<int, array{name: string, label: string}>
|
||||
*/
|
||||
private function activeGateways(bool $testMode): array
|
||||
{
|
||||
if ($testMode) {
|
||||
return [['name' => 'mellat', 'label' => 'بانک ملت (آزمایشی)']];
|
||||
}
|
||||
|
||||
$labels = ['mellat' => 'بانک ملت', 'sep' => 'سپ (سامان کیش)'];
|
||||
$gateways = [];
|
||||
foreach ([$this->mellat, $this->sep] as $gateway) {
|
||||
$name = $gateway->getName();
|
||||
if ($gateway->isConfigured() && $this->isGatewayEnabled($name)) {
|
||||
$gateways[] = ['name' => $name, 'label' => $labels[$name] ?? $name];
|
||||
}
|
||||
}
|
||||
return $gateways;
|
||||
}
|
||||
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[Route('/api/v1/my/payments', methods: ['GET'])]
|
||||
public function myPayments(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
@@ -675,35 +426,11 @@ HTML;
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
|
||||
return $this->success(['data' => $payment->toArray()]);
|
||||
return $this->success($payment->toArray());
|
||||
}
|
||||
|
||||
// ── Private helpers ───────────────────────────────────────────────────────
|
||||
|
||||
private function resolveGateway(string $name): \App\Payment\Gateway\PaymentGatewayInterface|null
|
||||
{
|
||||
if ($this->configRepo->get('payment_test_mode') === '1') {
|
||||
return $this->mock;
|
||||
}
|
||||
|
||||
if (!$this->isGatewayEnabled($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match ($name) {
|
||||
'mellat' => $this->mellat,
|
||||
'sep' => $this->sep,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/** آیا این درگاه در تنظیمات فعال است؟ کلید تنظیمنشده = فعال (سازگاری با نصبهای قبلی). */
|
||||
private function isGatewayEnabled(string $name): bool
|
||||
{
|
||||
$v = $this->configRepo->get($name . '_enabled');
|
||||
return $v === null || $v === '1';
|
||||
}
|
||||
|
||||
/** @return string[] allowed frontend hosts — from SiteConfig, falling back to env. */
|
||||
private function allowedHosts(): array
|
||||
{
|
||||
@@ -743,76 +470,6 @@ HTML;
|
||||
return false;
|
||||
}
|
||||
|
||||
private function handleSmsWalletCharge(Payment $payment): void
|
||||
{
|
||||
$meta = $payment->getMetadata() ?? [];
|
||||
$entityType = $meta['entity_type'] ?? null;
|
||||
$entityId = isset($meta['entity_id']) ? (int) $meta['entity_id'] : null;
|
||||
|
||||
if ($entityType === null || $entityId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wallet = $this->smsWalletService->getOrCreate($entityType, $entityId);
|
||||
$this->smsWalletService->charge($wallet, $payment->getAmountRials(), $payment);
|
||||
}
|
||||
|
||||
private function handleAppointmentConfirmation(Payment $payment): void
|
||||
{
|
||||
$appointment = $payment->getAppointment();
|
||||
if ($appointment === null || !$appointment->canTransitionTo(Appointment::STATUS_CONFIRMED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$appointment->transitionTo(Appointment::STATUS_CONFIRMED);
|
||||
$this->appointmentRepo->save($appointment);
|
||||
|
||||
$doctor = $appointment->getDoctor();
|
||||
$this->commissionService->processAppointment(
|
||||
$payment,
|
||||
$doctor->getRepresentationId(),
|
||||
$appointment->getBookingRepresentationId(),
|
||||
$doctor->getId(),
|
||||
);
|
||||
|
||||
$mobile = $appointment->getPatientMobile();
|
||||
if ($mobile) {
|
||||
$when = $this->jalali->formatDateTime($appointment->getSlotStart());
|
||||
$message = $this->smsText->resolve(\App\Sms\Entity\SmsLog::TAG_PAYMENT, [
|
||||
'doctor' => $appointment->getDoctor()->getName(),
|
||||
'date' => $when,
|
||||
]);
|
||||
$this->smsService->dispatchAsync(
|
||||
$mobile,
|
||||
$message,
|
||||
tag: \App\Sms\Entity\SmsLog::TAG_PAYMENT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleSubscriptionActivation(Payment $payment): void
|
||||
{
|
||||
$meta = $payment->getMetadata() ?? [];
|
||||
$periodUuid = $meta['period_uuid'] ?? null;
|
||||
if ($periodUuid === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $payment->getUser();
|
||||
$doctor = $this->doctorRepo->findByUser($user);
|
||||
if ($doctor !== null) {
|
||||
$this->subscriptionService->createFromPayment($payment, 'doctor', $doctor->getId(), $periodUuid);
|
||||
$this->commissionService->processSubscription($payment, $doctor->getRepresentationId(), $doctor->getId(), null);
|
||||
return;
|
||||
}
|
||||
|
||||
$clinic = $this->clinicRepo->findByUser($user);
|
||||
if ($clinic !== null) {
|
||||
$this->subscriptionService->createFromPayment($payment, 'clinic', $clinic->getId(), $periodUuid);
|
||||
$this->commissionService->processSubscription($payment, $clinic->getRepresentationId(), null, $clinic->getId());
|
||||
}
|
||||
}
|
||||
|
||||
private function redirectToFrontend(Payment $payment, bool $success): \Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
$base = $payment->getFrontendAddress();
|
||||
|
||||
Reference in New Issue
Block a user