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:
hamed
2026-07-02 15:36:08 +03:30
parent ca71c49451
commit c247ac2c80
12 changed files with 761 additions and 378 deletions
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace App\Payment\Gateway;
use App\Config\Repository\SiteConfigRepository;
/**
* انتخاب/ساخت درگاه پرداخت (Factory + Strategy).
*
* تمام منطقِ «کدام درگاه، فعال است یا نه، حالت تست» اینجا متمرکز است تا کنترلر
* فقط Orchestration کند. افزودن درگاه جدید = ثبت آن به‌عنوان سرویس و اضافه‌کردن
* یک case در map + یک برچسب، بدون تغییر کنترلر.
*/
class GatewayFactory
{
/** @var array<string, PaymentGatewayInterface> */
private array $gateways;
/** @var array<string, string> برچسب فارسی هر درگاه */
private const LABELS = [
'mellat' => 'بانک ملت',
'sep' => 'سپ (سامان کیش)',
];
public function __construct(
MellatGateway $mellat,
SepGateway $sep,
private readonly MockGateway $mock,
private readonly SiteConfigRepository $configRepo,
) {
$this->gateways = [
$mellat->getName() => $mellat,
$sep->getName() => $sep,
];
}
public function isTestMode(): bool
{
return $this->configRepo->get('payment_test_mode') === '1';
}
/**
* درگاهِ قابل‌استفاده برای این نام؛ در حالت تست همیشه Mock، در غیر این‌صورت
* درگاه واقعی در صورت فعال بودن. null یعنی نامعتبر/غیرفعال.
*/
public function resolve(string $name): ?PaymentGatewayInterface
{
if ($this->isTestMode()) {
return $this->mock;
}
if (!$this->isEnabled($name)) {
return null;
}
return $this->gateways[$name] ?? null;
}
/** آیا درگاه در تنظیمات فعال است؟ کلید تنظیم‌نشده = فعال (سازگاری با نصب‌های قبلی). */
public function isEnabled(string $name): bool
{
$v = $this->configRepo->get($name . '_enabled');
return $v === null || $v === '1';
}
/**
* درگاه‌های قابل‌انتخاب برای نمایش به کاربر: در حالت تست فقط درگاه آزمایشی،
* در غیر این‌صورت هر درگاهی که اعتبارنامه‌اش ست شده و فعال است.
*
* @return array<int, array{name: string, label: string}>
*/
public function activeGateways(): array
{
if ($this->isTestMode()) {
return [['name' => 'mellat', 'label' => 'بانک ملت (آزمایشی)']];
}
$out = [];
foreach ($this->gateways as $name => $gateway) {
if ($gateway->isConfigured() && $this->isEnabled($name)) {
$out[] = ['name' => $name, 'label' => self::LABELS[$name] ?? $name];
}
}
return $out;
}
}