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,67 @@
|
||||
# معماری — تسک ۱۵: ماژول پرداخت
|
||||
|
||||
## ساختار فایلها
|
||||
```
|
||||
src/Module/Payment/
|
||||
├── Controller/
|
||||
│ ├── PaymentController.php ← ایجاد و دریافت پرداخت
|
||||
│ └── PaymentCallbackController.php ← callback درگاه پرداخت
|
||||
├── Service/
|
||||
│ ├── PaymentService.php
|
||||
│ └── Gateway/
|
||||
│ ├── PaymentGatewayInterface.php
|
||||
│ ├── ZarinpalGateway.php ← درگاه زرینپال
|
||||
│ └── NullGateway.php ← برای محیط dev
|
||||
├── Repository/
|
||||
│ └── PaymentRepository.php
|
||||
├── Entity/
|
||||
│ └── Payment.php
|
||||
├── DTO/
|
||||
│ ├── Request/
|
||||
│ │ └── CreatePaymentRequest.php
|
||||
│ └── Response/
|
||||
│ └── PaymentResponse.php
|
||||
└── Voter/
|
||||
└── PaymentVoter.php
|
||||
```
|
||||
|
||||
## Entity: Payment
|
||||
```php
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'payments')]
|
||||
class Payment
|
||||
{
|
||||
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
|
||||
private int $id;
|
||||
|
||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||
private Uuid $uuid;
|
||||
|
||||
#[ORM\OneToOne(targetEntity: Appointment::class)]
|
||||
private Appointment $appointment;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
private User $user;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $amount; // ریال
|
||||
|
||||
// pending, paid, failed, refunded
|
||||
#[ORM\Column(length: 20, default: 'pending')]
|
||||
private string $status;
|
||||
|
||||
#[ORM\Column(length: 30, nullable: true)]
|
||||
private ?string $paymentMethod; // online, cash, insurance
|
||||
|
||||
#[ORM\Column(length: 100, nullable: true)]
|
||||
private ?string $gatewayToken; // توکن درگاه
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
private ?string $referenceCode; // کد پیگیری
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $paidAt;
|
||||
|
||||
// TimestampableTrait
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,107 @@
|
||||
# پایگاه داده — تسک ۱۵: ماژول پرداخت
|
||||
|
||||
## جدول: payments
|
||||
_(entity_type=payment, bundle=appointment — از DB backup تأیید شده)_
|
||||
|
||||
| ستون | نوع | نام Drupal | توضیح |
|
||||
|------|-----|-----------|-------|
|
||||
| id | INT UNSIGNED AUTO_INCREMENT PK | id | |
|
||||
| uuid | CHAR(36) UNIQUE NOT NULL | uuid | |
|
||||
| user_id | INT FK → users.id NOT NULL | uid | پرداختکننده |
|
||||
| appointment_id | INT FK → appointments.id UNIQUE NULL | field_reference_id | entity ref → appointment |
|
||||
| representation_id | INT FK → representations.id NULL | field_representation | entity ref → clinic_pro |
|
||||
| amount | INT NOT NULL | field_amount | مبلغ به **ریال** (نه تومان) — تایپ INT |
|
||||
| status | VARCHAR(20) DEFAULT 'pending' | field_status | وضعیت |
|
||||
| payment_method | VARCHAR(20) NULL | field_payment_method | روش پرداخت |
|
||||
| ref_id | VARCHAR(100) NULL | field_ref_id | SaleReferenceId بانک |
|
||||
| frontend_address | VARCHAR(150) NULL | field_frontend_address | URL فرانت برای redirect |
|
||||
| payment_time | INT NULL | field_payment_time | زمان پرداخت (Unix timestamp) |
|
||||
| card_info | LONGTEXT NULL | field_card_info | اطلاعات کارت (JSON/text) |
|
||||
| created_at | INT NOT NULL | created | Unix timestamp |
|
||||
| updated_at | INT NOT NULL | changed | Unix timestamp |
|
||||
|
||||
## وضعیتهای پرداخت (field_status) — از config
|
||||
```
|
||||
pending → ایجاد شده، منتظر پرداخت
|
||||
received → پرداخت موفق تأیید شده ⚠️ نه 'paid'!
|
||||
refund → مبلغ برگشت خورده
|
||||
canceled → لغو شده (نه 'failed')
|
||||
```
|
||||
|
||||
## روشهای پرداخت (field_payment_method) — از config
|
||||
```
|
||||
mellat → بانک ملت (SOAP)
|
||||
sep → بانک سامان (SEP)
|
||||
```
|
||||
|
||||
## نمونه داده واقعی از DB backup
|
||||
```
|
||||
id=2, uid=33, amount=100000 (ریال), status=pending, method=mellat
|
||||
frontend_address='http://yasuj-nobat.localhost:3000/'
|
||||
```
|
||||
|
||||
## ایندکسها
|
||||
```sql
|
||||
CREATE UNIQUE INDEX idx_payments_appointment ON payments(appointment_id);
|
||||
CREATE INDEX idx_payments_user ON payments(user_id);
|
||||
CREATE INDEX idx_payments_status ON payments(status);
|
||||
CREATE INDEX idx_payments_representation ON payments(representation_id);
|
||||
CREATE INDEX idx_payments_ref_id ON payments(ref_id);
|
||||
```
|
||||
|
||||
## وابستگی وضعیت appointment
|
||||
```
|
||||
appointments.status = 'waiting_for_payment' → پیشنیاز ایجاد payment
|
||||
بعد از پرداخت موفق:
|
||||
payments.status = 'received'
|
||||
appointments.status = 'reserved'
|
||||
payment_time = Unix timestamp الان
|
||||
```
|
||||
|
||||
## نکات مهم
|
||||
- `amount` نوع **INT** است (نه DECIMAL) — از DB backup تأیید شد (مثال: 100000)
|
||||
- مبلغ در **ریال** ذخیره میشود
|
||||
- `field_reference_id` → entity reference به appointment (نه foreign key مستقیم در جدول payment)
|
||||
- `frontend_address` برای redirect بعد از پرداخت به سایت نماینده است
|
||||
- `card_info` برای ذخیره اطلاعات کارت بانکی (مثلاً شماره کارت ماسکشده)
|
||||
|
||||
---
|
||||
|
||||
## جدول: subscription_payments (پرداخت اشتراک)
|
||||
_(از بخش ۲.۱۰.۲ مستند — نوع پرداخت مجزا از پرداخت نوبت)_
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| id | INT UNSIGNED AUTO_INCREMENT PK | |
|
||||
| uuid | CHAR(36) UNIQUE NOT NULL | |
|
||||
| user_id | INT FK → users.id NOT NULL | کاربر سابسکرایبکننده |
|
||||
| reference_type | VARCHAR(10) NOT NULL | `doctor` یا `clinic` |
|
||||
| reference_id | INT NOT NULL | FK به doctors.id یا clinics.id |
|
||||
| representation_id | INT FK → representations.id NULL | نماینده (در صورت وجود) |
|
||||
| amount | INT NOT NULL | مبلغ اشتراک (ریال) |
|
||||
| payment_method | VARCHAR(20) NULL | روش پرداخت (mellat, sep, ...) |
|
||||
| payment_time | INT NULL | زمان پرداخت (Unix timestamp) |
|
||||
| start_date | INT NOT NULL | تاریخ شروع اشتراک (Unix timestamp) |
|
||||
| expiration_date | INT NOT NULL | تاریخ انقضای اشتراک (Unix timestamp) |
|
||||
| ref_id | VARCHAR(100) NULL | شماره مرجع درگاه بانکی |
|
||||
| card_info | LONGTEXT NULL | اطلاعات کارت بانکی (JSON) |
|
||||
| frontend_address | VARCHAR(150) NULL | آدرس بازگشت پس از پرداخت |
|
||||
| status | VARCHAR(20) DEFAULT 'pending' | pending \| received \| refund \| canceled |
|
||||
| created_at | INT NOT NULL | Unix timestamp |
|
||||
| updated_at | INT NOT NULL | Unix timestamp |
|
||||
|
||||
## ایندکسهای subscription_payments
|
||||
```sql
|
||||
CREATE INDEX idx_sub_payments_user ON subscription_payments(user_id);
|
||||
CREATE INDEX idx_sub_payments_ref ON subscription_payments(reference_type, reference_id);
|
||||
CREATE INDEX idx_sub_payments_status ON subscription_payments(status);
|
||||
CREATE INDEX idx_sub_payments_expiry ON subscription_payments(expiration_date);
|
||||
```
|
||||
|
||||
## تفاوت payments و subscription_payments
|
||||
|
||||
| ویژگی | payments | subscription_payments |
|
||||
|-------|----------|----------------------|
|
||||
| مرجع | `appointment_id` | `reference_id` → doctor/clinic |
|
||||
| فیلدهای اضافه | — | `start_date`, `expiration_date` |
|
||||
| هدف | پرداخت نوبت | خرید اشتراک پلن |
|
||||
@@ -0,0 +1,99 @@
|
||||
# نکات پیادهسازی — تسک ۱۵: ماژول پرداخت
|
||||
|
||||
## درگاههای واقعی پروژه (از کد Drupal)
|
||||
|
||||
### ۱. بانک ملت (Mellat) — پروتکل SOAP
|
||||
```php
|
||||
// وبسرویس SOAP با متدهای:
|
||||
// bpPayRequest → شروع تراکنش
|
||||
// bpVerifyRequest → تأیید پرداخت
|
||||
// bpInquiryRequest → استعلام وضعیت
|
||||
// bpSettleRequest → تسویه
|
||||
// bpReversalRequest → برگشت تراکنش
|
||||
|
||||
// پارامترهای پیکربندی:
|
||||
terminal_id, username, password
|
||||
wsdl_endpoint, gate_url
|
||||
test_mode (boolean)
|
||||
callback_url, callback_url_test
|
||||
```
|
||||
|
||||
### ۲. SEP (سامان) — درگاه دوم
|
||||
در `sep_payment/src/Plugin/MyPayment/SepPayment.php` پیادهسازی شده.
|
||||
|
||||
### پیادهسازی در Symfony
|
||||
```php
|
||||
interface PaymentGatewayInterface {
|
||||
public function pay(array $data): array; // { success, ref_id, gateway_url }
|
||||
public function verify(array $callbackData, int $orderId): array;
|
||||
public function refund(array $data): array;
|
||||
}
|
||||
```
|
||||
|
||||
پیکربندی در `.env`:
|
||||
```
|
||||
PAYMENT_GATEWAY=mellat # mellat | sep
|
||||
MELLAT_TERMINAL_ID=...
|
||||
MELLAT_USERNAME=...
|
||||
MELLAT_PASSWORD=...
|
||||
MELLAT_TEST_MODE=true
|
||||
```
|
||||
|
||||
## وضعیت پرداخت (از کد واقعی)
|
||||
```
|
||||
pending → بعد از ایجاد پرداخت
|
||||
received → بعد از تأیید موفق (نه "paid"!)
|
||||
failed → پرداخت ناموفق
|
||||
```
|
||||
⚠️ در Drupal status موفق `received` است نه `paid`.
|
||||
|
||||
## شرط ایجاد پرداخت
|
||||
**appointment باید status=`waiting_for_payment` داشته باشد.**
|
||||
اگر status متفاوت باشد → 400 error.
|
||||
|
||||
## قیمت از Config (نه Request)
|
||||
```php
|
||||
// مبلغ از پیکربندی خوانده میشود، نه از request body!
|
||||
$amount = $config->get('payment.settings')['price'];
|
||||
```
|
||||
→ در `.env` یا config:
|
||||
```
|
||||
APPOINTMENT_PRICE=500000
|
||||
```
|
||||
|
||||
## frontend_address
|
||||
پرداخت دارای `frontend_address` است — URL فرانت برای redirect بعد از پرداخت.
|
||||
این به representation مرتبط است (سیستم multi-tenant).
|
||||
|
||||
## Callback Mellat
|
||||
```
|
||||
POST /payment/callback/mellat
|
||||
RefId=...
|
||||
ResCode=0
|
||||
SaleOrderId=...
|
||||
SaleReferenceId=...
|
||||
|
||||
جریان:
|
||||
1. ResCode === '0' باشد
|
||||
2. bpVerifyRequest → اگر موفق نبود → bpInquiryRequest → اگر موفق نبود → bpReversalRequest
|
||||
3. bpSettleRequest (resCode='0' یا '45' = قبلاً تسویه شده)
|
||||
4. appointment.status = confirmed
|
||||
5. payment.status = received
|
||||
6. payment.ref_id = SaleReferenceId
|
||||
```
|
||||
|
||||
## Idempotency
|
||||
اگر callback دوبار بیاید، دوبار process نشود:
|
||||
```php
|
||||
if ($payment->getStatus() === 'received') {
|
||||
return; // قبلاً پردازش شده
|
||||
}
|
||||
```
|
||||
|
||||
## مجوزها
|
||||
```
|
||||
POST /payment → احراز هویتشده
|
||||
GET /payment/{uuid} → owner یا ROLE_ADMIN یا دکتر مرتبط
|
||||
GET /my-payments → owner
|
||||
GET/POST callback → عمومی (درگاه پرداخت)
|
||||
```
|
||||
@@ -0,0 +1,285 @@
|
||||
# تسک ۱۵: ماژول پرداخت
|
||||
|
||||
## توضیح
|
||||
مدیریت پرداخت نوبتها از طریق درگاههای Mellat و SEP،
|
||||
callback پرداخت، refund و مشاهده تاریخچه.
|
||||
|
||||
## Endpoint ها
|
||||
|
||||
| متد | مسیر | توضیح | نیاز به Auth |
|
||||
|-----|------|-------|-------------|
|
||||
| POST | `/api/v1/payment` | شروع فرآیند پرداخت | بله |
|
||||
| GET | `/api/v1/payment/{uuid}` | دریافت اطلاعات پرداخت | بله |
|
||||
| GET | `/api/v1/payment/my-payments/{userId}` | تاریخچه پرداختهای من | بله |
|
||||
| POST | `/api/v1/payment/callback/mellat` | Callback از درگاه ملت | خیر (IP whitelist) |
|
||||
| POST | `/api/v1/payment/callback/sep` | Callback از درگاه سامان | خیر (IP whitelist) |
|
||||
| POST | `/api/v1/subscription-payment` | شروع پرداخت اشتراک | بله |
|
||||
| GET | `/api/v1/subscription-payment/{uuid}` | اطلاعات پرداخت اشتراک | بله |
|
||||
| POST | `/api/v1/subscription-payment/callback/mellat` | Callback اشتراک ملت | خیر |
|
||||
| POST | `/api/v1/subscription-payment/callback/sep` | Callback اشتراک سامان | خیر |
|
||||
|
||||
## پیشنیازها
|
||||
- تسک ۰۱، ۰۲، ۱۰ (Appointment)
|
||||
|
||||
## زمان تخمینی
|
||||
۱۰ تا ۱۲ ساعت
|
||||
|
||||
---
|
||||
|
||||
## فلوی کامل پرداخت نوبت
|
||||
|
||||
```
|
||||
۱. POST /api/v1/payment
|
||||
↓
|
||||
۲. بررسی: appointment.status == 'waiting_for_payment' ؟
|
||||
↓ (بله)
|
||||
۳. ایجاد رکورد payment با status=pending
|
||||
↓
|
||||
۴. فراخوانی PaymentGatewayInterface::initiate(amount, callback_url)
|
||||
↓
|
||||
┌──────────────────┬──────────────────┐
|
||||
Mellat (SOAP) SEP (REST)
|
||||
→ bpPayRequest → MerchantSendTransaction
|
||||
→ دریافت RefId → دریافت token
|
||||
↓
|
||||
۵. بازگشت payment_url به کلاینت
|
||||
↓
|
||||
۶. Redirect کاربر به درگاه بانک
|
||||
↓
|
||||
۷. [Callback از بانک]
|
||||
↓
|
||||
۸. POST /api/v1/payment/callback/{gateway}
|
||||
↓
|
||||
۹. تأیید تراکنش با درگاه (VerifyRequest)
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
پرداخت موفق پرداخت ناموفق
|
||||
↓ ↓
|
||||
payments.status=received payments.status=canceled
|
||||
appointments.status=reserved appointments.status=waiting_for_payment
|
||||
واریز کمیسیون نماینده (کاربر میتواند مجدداً تلاش کند)
|
||||
↓
|
||||
Redirect به frontend_address
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Strategy Pattern برای درگاهها
|
||||
|
||||
```php
|
||||
interface PaymentGatewayInterface
|
||||
{
|
||||
public function initiate(int $amount, string $callbackUrl, string $description): GatewayInitResult;
|
||||
public function verify(string $refId, int $amount): GatewayVerifyResult;
|
||||
public function getName(): string; // 'mellat' | 'sep'
|
||||
}
|
||||
|
||||
class MellatGateway implements PaymentGatewayInterface { ... }
|
||||
class SepGateway implements PaymentGatewayInterface { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /api/v1/payment
|
||||
|
||||
```json
|
||||
// Request
|
||||
{
|
||||
"appointment_uuid": "7b759d2a-...",
|
||||
"payment_method": "mellat",
|
||||
"frontend_address": "https://yasuj-nobat.localhost:3000/"
|
||||
}
|
||||
|
||||
// Response 200
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"uuid": "...",
|
||||
"payment_url": "https://bpm.shaparak.ir/pgwchannel/startpay.mellat?RefId=xxx",
|
||||
"amount": 500000,
|
||||
"status": "pending",
|
||||
"expires_at": 1748001800
|
||||
}
|
||||
}
|
||||
|
||||
// Response 400 — نوبت در وضعیت نامناسب
|
||||
{
|
||||
"success": false,
|
||||
"errors": [{ "code": "ERR_PAYMENT_003", "message": "وضعیت نوبت برای پرداخت مناسب نیست" }]
|
||||
}
|
||||
|
||||
// Response 503 — درگاه در دسترس نیست
|
||||
{
|
||||
"success": false,
|
||||
"errors": [{ "code": "ERR_PAYMENT_001", "message": "درگاه پرداخت در حال حاضر در دسترس نیست" }]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /api/v1/payment/callback/mellat
|
||||
|
||||
```
|
||||
// form-data از بانک
|
||||
ResCode=0
|
||||
SaleOrderId=...
|
||||
SaleReferenceId=12345678
|
||||
```
|
||||
|
||||
**منطق:**
|
||||
```
|
||||
1. پیدا کردن payment با ref_id مربوطه
|
||||
2. فراخوانی MellatGateway::verify(SaleReferenceId, amount)
|
||||
3. اگر موفق:
|
||||
- payments.status = 'received'
|
||||
- payments.ref_id = SaleReferenceId
|
||||
- payments.payment_time = now()
|
||||
- appointments.status = 'reserved'
|
||||
- محاسبه و واریز کمیسیون نماینده (async)
|
||||
4. Redirect به frontend_address + ?status=success
|
||||
5. اگر ناموفق:
|
||||
- payments.status = 'canceled'
|
||||
- Redirect به frontend_address + ?status=failed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/payment/{uuid}
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"uuid": "...",
|
||||
"appointment": {
|
||||
"uuid": "...",
|
||||
"date": "2024-03-20",
|
||||
"time": "09:00",
|
||||
"doctor": { "name": "دکتر احمدی" }
|
||||
},
|
||||
"amount": 500000,
|
||||
"status": "received",
|
||||
"payment_method": "mellat",
|
||||
"ref_id": "12345678",
|
||||
"payment_time": 1748000000,
|
||||
"created_at": 1748000000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## فلوی Refund (لغو نوبت بعد از پرداخت)
|
||||
|
||||
```
|
||||
PATCH /api/v1/appointment/{uuid}/cancel
|
||||
↓
|
||||
appointment.status = 'cancelled_by_patient'
|
||||
↓
|
||||
payment.status = 'refund'
|
||||
↓
|
||||
ثبت در سیستم — refund واقعی دستی توسط ادمین انجام میشود
|
||||
↓
|
||||
log در سیستم برای پیگیری ادمین
|
||||
```
|
||||
|
||||
> **نکته:** Refund خودکار از درگاه در این پروژه پیادهسازی نمیشود — ادمین به صورت دستی مبلغ را برمیگرداند.
|
||||
|
||||
---
|
||||
|
||||
## Subscription Payment — POST /api/v1/subscription-payment
|
||||
|
||||
```json
|
||||
// Request
|
||||
{
|
||||
"reference_type": "doctor",
|
||||
"reference_id": 29,
|
||||
"plan": "advanced",
|
||||
"payment_method": "mellat",
|
||||
"frontend_address": "https://yasuj-nobat.localhost:3000/"
|
||||
}
|
||||
|
||||
// Response 200
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"uuid": "...",
|
||||
"payment_url": "https://bpm.shaparak.ir/...",
|
||||
"amount": 5000000,
|
||||
"plan": "advanced",
|
||||
"status": "pending"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**بعد از تأیید پرداخت اشتراک:**
|
||||
```
|
||||
subscription_payments.status = 'received'
|
||||
subscription_payments.start_date = now()
|
||||
subscription_payments.expiration_date = now() + 30 روز (یا 365 روز)
|
||||
واریز کمیسیون به کیف پول نماینده (اگر از طریق نماینده)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **مبلغ در ریال ذخیره میشود** (نه تومان) — مثال: ۵۰,۰۰۰ تومان = ۵۰۰,۰۰۰ ریال
|
||||
- **وضعیت 'received'** — نه 'paid' (مستقیم از Drupal)
|
||||
- **Circuit Breaker:** اگر درگاه ۳ بار پشت سر هم fail داشت → به مدت ۵ دقیقه blocked شود
|
||||
- **Idempotency:** Callback ممکن است چند بار فراخوانی شود — بررسی کنید payment قبلاً verified نشده باشد
|
||||
- **IP Whitelist:** Callback endpoint ها باید فقط از IP های بانک قابل دسترس باشند
|
||||
|
||||
---
|
||||
|
||||
## ⚠ امنیت: جلوگیری از Open Redirect
|
||||
|
||||
فیلد `frontend_address` در request میتواند توسط مهاجم دستکاری شود تا Callback به یک سایت مخرب redirect کند.
|
||||
|
||||
**راهحل — Whitelist دامنههای مجاز:**
|
||||
|
||||
```php
|
||||
// config/packages/payment.yaml (یا .env)
|
||||
ALLOWED_FRONTEND_HOSTS=yasuj-nobat.localhost,clinicpro.ir,app.clinicpro.ir
|
||||
|
||||
// در PaymentService قبل از ذخیره frontend_address:
|
||||
private function validateFrontendAddress(string $url): void
|
||||
{
|
||||
$parsed = parse_url($url);
|
||||
$host = $parsed['host'] ?? '';
|
||||
$allowed = explode(',', $this->params->get('allowed_frontend_hosts'));
|
||||
|
||||
if (!in_array($host, $allowed, true)) {
|
||||
throw new \InvalidArgumentException('آدرس بازگشت مجاز نیست');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**یا روش سادهتر:** `frontend_address` را از JWT کاربر یا از `representations.domain_name` بخوان — نه از request body.
|
||||
|
||||
---
|
||||
|
||||
## ⚠ امنیت: IP Whitelist برای Callback
|
||||
|
||||
```php
|
||||
// src/Payment/EventSubscriber/PaymentCallbackGuard.php
|
||||
class PaymentCallbackGuard implements EventSubscriberInterface
|
||||
{
|
||||
private const MELLAT_IPS = ['185.143.233.0/24', '79.175.148.0/24'];
|
||||
private const SEP_IPS = ['195.146.48.0/24'];
|
||||
|
||||
public function onKernelRequest(RequestEvent $event): void
|
||||
{
|
||||
$path = $event->getRequest()->getPathInfo();
|
||||
if (!str_contains($path, '/payment/callback/')) return;
|
||||
|
||||
$clientIp = $event->getRequest()->getClientIp();
|
||||
$gateway = str_contains($path, 'mellat') ? 'mellat' : 'sep';
|
||||
$allowed = $gateway === 'mellat' ? self::MELLAT_IPS : self::SEP_IPS;
|
||||
|
||||
if (!$this->ipInRanges($clientIp, $allowed)) {
|
||||
throw new AccessDeniedHttpException('IP not allowed for payment callback');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user