Feat add payment with gateway of montify

This commit is contained in:
mahmood
2026-07-24 16:50:21 +03:30
parent eacf5181a9
commit 572815a6a9
13 changed files with 1710 additions and 14 deletions
+478
View File
@@ -0,0 +1,478 @@
# راهنمای اتصال پرداخت خارجی (Partner Integration)
این سند برای تیم فنی سایت‌هایی است که می‌خواهند کاربر را به Montify بفرستند، پرداخت را از درگاه انجام دهند، و نتیجه را امن دریافت کنند.
---
## ۱. خلاصه جریان
```
سایت شما (سرور)
│ POST /external-payment/initiate (امضای HMAC)
▼
Montify → payment_url
│
▼ redirect کاربر
/pay/external/{token} → درگاه پرداخت
│
▼ برگشت از درگاه
Montify verify
│
├─► redirect مرورگر به return_url شما (با signature)
└─► POST webhook به سرور شما (با signature)
```
**نکات مهم:**
- کاربر نهایی نیاز به لاگین در Montify ندارد.
- مبلغ و آدرس برگشت فقط از سرور شما (با امضا) پذیرفته می‌شود؛ لینک دستی/جعلی کار نمی‌کند.
- `client_secret` فقط روی سرور شما بماند؛ هرگز در فرانت‌اند قرار ندهید.
---
## ۲. پیش‌نیازها
از ادمین Montify این اطلاعات را دریافت کنید:
| مقدار | توضیح |
|--------|--------|
| `BASE_URL` | آدرس API نود، مثلاً `https://node.example.com` |
| `APP_URL` | آدرس فرانت Montify (برای `payment_url`) |
| `client_id` | شناسه partner شما |
| `client_secret` | رمز مخفی (فقط یک‌بار نمایش داده می‌شود) |
همچنین این موارد باید در پنل partner ثبت شده باشد:
- `allowed_return_urls` — پیشوند آدرس‌های مجاز برگشت (مثلاً `https://yoursite.com/payment/`)
- `webhook_url` — اختیاری؛ آدرس پیش‌فرض webhook شما
- `allowed_ips` — اختیاری؛ اگر پر باشد فقط همان IPها مجازند
---
## ۳. احراز هویت درخواست‌های سرور به سرور
برای `initiate` و `status` این هدرها الزامی است:
| Header | توضیح |
|--------|--------|
| `X-Client-Id` | همان `client_id` |
| `X-Timestamp` | Unix time به ثانیه |
| `X-Nonce` | یکتا برای هر درخواست (مثلاً UUID) |
| `X-Signature` | HMAC-SHA256 روی canonical string |
| `Content-Type` | `application/json` |
### ۳.۱. ساخت Canonical String
```
METHOD
PATH
BODY_HASH
TIMESTAMP
NONCE
```
با `\n` بین خطوط. بدون `\n` انتهایی.
- `METHOD`: حروف بزرگ (`POST` یا `GET`)
- `PATH`: فقط path بدون query، مثلاً `/external-payment/initiate`
- `BODY_HASH`: `SHA256` از `stableStringify(body)` به صورت hex
برای `GET` بدون body: hash روی رشته خالی `""`
- `TIMESTAMP` و `nonce`: همان مقادیر هدر
### ۳.۲. stableStringify
قبل از hash کردن body، JSON را با کلیدهای **مرتب‌شده** بسازید:
- object: کلیدها به ترتیب الفبایی
- array: ترتیب حفظ شود
- null / number / string / boolean: مثل `JSON.stringify`
مثال:
```json
{"amount":1000,"gateway":"zarinpall","reference_id":"ORD-1","return_url":"https://yoursite.com/ok"}
```
نه:
```json
{"return_url":"...","amount":1000,...}
```
### ۳.۳. Signature
```text
X-Signature = HMAC_SHA256(canonical, client_secret) → hex
```
### ۳.۴. محدودیت‌ها
- اختلاف ساعت مجاز: ±۵ دقیقه
- هر `nonce` فقط یک‌بار قابل استفاده است (ضد replay)
---
## ۴. APIها
### ۴.۱. شروع پرداخت — `POST /external-payment/initiate`
**Auth:** HMAC (الزامی)
**Body:**
| فیلد | نوع | الزامی | توضیح |
|------|-----|--------|--------|
| `amount` | number | بله | مبلغ به **ریال** (حداقل ۱۰۰۰) |
| `reference_id` | string | بله | شناسه سفارش شما (یکتا در وضعیت فعال) |
| `gateway` | string | بله | یکی از: `zarinpall` ، `zibal` ، `payping` ، `sepal` |
| `return_url` | string | بله | باید با یکی از `allowed_return_urls` شروع شود |
| `webhook_url` | string | خیر | اگر نباشد از webhook پیش‌فرض partner استفاده می‌شود |
| `description` | string | خیر | توضیح |
| `metadata` | object | خیر | داده دلخواه شما |
**نمونه درخواست:**
```http
POST /external-payment/initiate
Content-Type: application/json
X-Client-Id: ptr_xxxxxxxx
X-Timestamp: 1720000000
X-Nonce: 550e8400-e29b-41d4-a716-446655440000
X-Signature: <hex>
{
"amount": 500000,
"reference_id": "ORDER-1001",
"gateway": "zarinpall",
"return_url": "https://yoursite.com/payment/result",
"webhook_url": "https://yoursite.com/api/payment/webhook",
"description": "پرداخت سفارش ۱۰۰۱"
}
```
**پاسخ موفق `201`:**
```json
{
"session_id": "a1b2c3...",
"payment_url": "https://shop.example.com/pay/external/a1b2c3...",
"expires_at": "2026-07-21T10:15:00.000Z",
"reference_id": "ORDER-1001"
}
```
کاربر را به `payment_url` هدایت کنید. جلسه حدود **۱۵ دقیقه** اعتبار دارد.
**خطاهای رایج:**
| error | معنی |
|-------|------|
| `invalid_signature` | امضا اشتباه است |
| `expired_timestamp` | ساعت سرور نامعتبر |
| `replay_detected` | nonce تکراری |
| `invalid_return_url` | `return_url` در whitelist نیست |
| `invalid_gateway` | درگاه نامعتبر |
| `amount_too_low` | مبلغ کمتر از ۱۰۰۰ ریال |
| `duplicate_reference` | `reference_id` قبلاً در وضعیت فعال استفاده شده |
| `gateway_not_configured` | درگاه در Montify پیکربندی نشده |
---
### ۴.۲. استعلام وضعیت — `GET /external-payment/status`
**Auth:** HMAC (الزامی)
یکی از queryها لازم است:
- `?reference_id=ORDER-1001`
- `?session_id=<session_id>`
**پاسخ نمونه:**
```json
{
"token": "a1b2c3...",
"reference_id": "ORDER-1001",
"amount": 500000,
"gateway": "zarinpall",
"status": "paid",
"track_number": "123456789",
"expires_at": "...",
"paid_at": "...",
"created_at": "..."
}
```
**وضعیت‌ها (`status`):**
| status | معنی |
|--------|------|
| `pending` | ساخته شده، هنوز به درگاه نرفته |
| `processing` | به درگاه هدایت شده |
| `paid` | پرداخت موفق |
| `failed` | ناموفق |
| `expired` | منقضی |
| `cancelled` | لغو |
> منبع حقیقت نهایی برای علامت‌گذاری سفارش: **Webhook** یا **استعلام status**. فقط به redirect مرورگر اکتفا نکنید.
---
## ۵. برگشت کاربر (Browser Redirect)
بعد از پرداخت، Montify کاربر را به `return_url` شما می‌فرستد با queryهای امضاشده:
```
https://yoursite.com/payment/result?
status=success
&reference_id=ORDER-1001
&session_id=a1b2c3...
&amount=500000
&track_number=123456789
&timestamp=1720000123
&signature=<hex>
```
| پارامتر | توضیح |
|---------|--------|
| `status` | `success` یا `failed` |
| `reference_id` | همان شناسه شما |
| `session_id` | توکن جلسه |
| `amount` | مبلغ (ریال) |
| `track_number` | کد پیگیری درگاه (ممکن است خالی باشد) |
| `timestamp` | زمان امضا |
| `signature` | HMAC روی payload |
### تأیید امضای redirect
1. همه پارامترها به‌جز `signature` را بگیرید.
2. یک object بسازید (مقادیر string هستند).
3. `stableStringify` کنید.
4. `HMAC-SHA256(..., client_secret)` بگیرید و با `signature` مقایسه کنید.
5. `timestamp` را چک کنید (±۵ دقیقه توصیه می‌شود).
6. مبلغ و `reference_id` را با سفارش خودتان تطبیق دهید.
---
## ۶. Webhook سرور به سرور
Montify بعد از پرداخت موفق یک `POST` به `webhook_url` می‌زند.
**Headers:**
```
Content-Type: application/json
X-Client-Id: ptr_xxxxxxxx
X-Timestamp: 1720000123
X-Signature: <hex>
```
**Body:**
```json
{
"status": "success",
"reference_id": "ORDER-1001",
"session_id": "a1b2c3...",
"amount": 500000,
"track_number": "123456789",
"gateway": "zarinpall",
"timestamp": "1720000123"
}
```
### تأیید امضای webhook
`X-Signature = HMAC-SHA256(stableStringify(body), client_secret)`
پاسخ `2xx` برگردانید. در غیر این صورت ممکن است ارسال ناموفق ثبت شود.
> Webhook فقط وقتی session به `paid` برسد ارسال می‌شود.
---
## ۷. نمونه کد Node.js
```js
const crypto = require('crypto');
const axios = require('axios');
const BASE_URL = 'https://node.example.com';
const CLIENT_ID = process.env.MONTIFY_CLIENT_ID;
const CLIENT_SECRET = process.env.MONTIFY_CLIENT_SECRET;
function stableStringify(obj) {
if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
if (Array.isArray(obj)) return `[${obj.map(stableStringify).join(',')}]`;
const keys = Object.keys(obj).sort();
return `{${keys.map((k) => `"${k}":${stableStringify(obj[k])}`).join(',')}}`;
}
function signRequest(method, path, body) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID();
const bodyString = body ? stableStringify(body) : '';
const bodyHash = crypto.createHash('sha256').update(bodyString).digest('hex');
const canonical = [method.toUpperCase(), path, bodyHash, timestamp, nonce].join('\n');
const signature = crypto.createHmac('sha256', CLIENT_SECRET).update(canonical).digest('hex');
return {
'Content-Type': 'application/json',
'X-Client-Id': CLIENT_ID,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
};
}
async function initiatePayment({ amount, referenceId, gateway, returnUrl, webhookUrl, description }) {
const path = '/external-payment/initiate';
const body = {
amount,
reference_id: referenceId,
gateway,
return_url: returnUrl,
webhook_url: webhookUrl,
description,
};
const headers = signRequest('POST', path, body);
const { data } = await axios.post(`${BASE_URL}${path}`, body, { headers });
return data; // { session_id, payment_url, expires_at, reference_id }
}
function verifyReturnSignature(query) {
const { signature, ...rest } = query;
const expected = crypto
.createHmac('sha256', CLIENT_SECRET)
.update(stableStringify(rest))
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
}
function verifyWebhook(body, signatureHeader) {
const expected = crypto
.createHmac('sha256', CLIENT_SECRET)
.update(stableStringify(body))
.digest('hex');
const a = Buffer.from(signatureHeader, 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// مثال استفاده:
// const result = await initiatePayment({
// amount: 500000,
// referenceId: 'ORDER-1001',
// gateway: 'zarinpall',
// returnUrl: 'https://yoursite.com/payment/result',
// webhookUrl: 'https://yoursite.com/api/payment/webhook',
// description: 'خرید سفارش',
// });
// res.redirect(result.payment_url);
```
---
## ۸. نمونه کد PHP
```php
<?php
function stable_stringify($obj) {
if (is_null($obj) || !is_array($obj)) {
return json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
// تشخیص array لیستی vs associative
if (array_is_list($obj)) {
$parts = array_map('stable_stringify', $obj);
return '[' . implode(',', $parts) . ']';
}
ksort($obj);
$parts = [];
foreach ($obj as $k => $v) {
$parts[] = '"' . $k . '":' . stable_stringify($v);
}
return '{' . implode(',', $parts) . '}';
}
function sign_request(string $method, string $path, ?array $body, string $clientId, string $secret): array {
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$bodyString = $body ? stable_stringify($body) : '';
$bodyHash = hash('sha256', $bodyString);
$canonical = strtoupper($method) . "\n" . $path . "\n" . $bodyHash . "\n" . $timestamp . "\n" . $nonce;
$signature = hash_hmac('sha256', $canonical, $secret);
return [
'Content-Type: application/json',
'X-Client-Id: ' . $clientId,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . $signature,
];
}
function initiate_payment(array $payload, string $baseUrl, string $clientId, string $secret): array {
$path = '/external-payment/initiate';
$headers = sign_request('POST', $path, $payload, $clientId, $secret);
$ch = curl_init(rtrim($baseUrl, '/') . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
CURLOPT_RETURNTRANSFER => true,
]);
$raw = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($raw, true);
if ($code >= 400) {
throw new RuntimeException($data['message'] ?? 'initiate failed');
}
return $data;
}
function verify_return_signature(array $query, string $secret): bool {
$signature = $query['signature'] ?? '';
unset($query['signature']);
$expected = hash_hmac('sha256', stable_stringify($query), $secret);
return hash_equals($expected, $signature);
}
```
---
## ۹. چک‌لیست پیاده‌سازی سایت شما
1. `client_id` / `client_secret` را فقط در env سرور نگه دارید.
2. قبل از پرداخت، سفارش خود را با وضعیت «در انتظار پرداخت» ذخیره کنید.
3. از سرور `initiate` بزنید و کاربر را به `payment_url` بفرستید.
4. صفحه `return_url` را بسازید و signature را verify کنید.
5. endpoint webhook بسازید و signature را verify کنید؛ سفارش را آنجا `paid` کنید.
6. برای اطمینان، در صورت نیاز `GET /status` هم بزنید (idempotent).
7. `reference_id` را یکتا و قابل ردیابی نگه دارید.
8. `return_url` را دقیقاً زیر پیشوند whitelist ثبت‌شده بفرستید.
---
## ۱۰. امنیت — خلاصه قوانین
| کار درست | کار غلط |
|----------|---------|
| initiate فقط از سرور | ساخت لینک پرداخت در مرورگر کاربر |
| امضا با `client_secret` | گذاشتن secret در JS فرانت |
| verify امضای redirect و webhook | اعتماد کور به query بدون signature |
| تطبیق `amount` با سفارش | قبول هر مبلغ برگشتی |
| whitelist برای `return_url` | قبول هر URL دلخواه |
---
## ۱۱. پشتیبانی
در صورت خطای مداوم `invalid_signature`:
1. ساعت سرور (NTP) را چک کنید.
2. `stableStringify` و ترتیب کلیدها را بررسی کنید.
3. `PATH` دقیقاً `/external-payment/initiate` باشد (بدون domain و بدون slash اضافه).
4. body ارسالی دقیقاً همان چیزی باشد که hash شده (نه serialize متفاوت axios/curl).
برای فعال‌سازی partner جدید یا افزودن دامنه به whitelist با ادمین Montify هماهنگ کنید.
+272
View File
@@ -0,0 +1,272 @@
# شارژ آنلاین کیف پول (Wallet Online Payment)
مستند پیاده‌سازی داخلی پروژه برای شارژ کیف پول از طریق درگاه.
برای پروتکل Montify (HMAC، initiate، webhook) به سند جداگانه مراجعه کنید:
→ [`EXTERNAL_PAYMENT_PARTNER.md`](./EXTERNAL_PAYMENT_PARTNER.md)
---
## ۱. هدف
- کاربر بتواند علاوه بر واریز دستی (کارت/شبا)، از **پرداخت آنلاین** کیف را شارژ کند.
- تمام credentialها و انتخاب درگاه **فقط سمت سرور** بماند.
- معماری طوری باشد که بعداً چند نوع درگاه (Montify پارتنر، درگاه مستقیم، …) اضافه شود.
---
## ۲. جریان کلی (از دید کاربر و سیستم)
```
اپ کاربر (khaneghi_user)
│ مبلغ را وارد می‌کند → مدال راهنمای شارژ
│ GET /customer/wallet/payment/options → { online_enabled }
│ POST /customer/wallet/payment/initiate → { payment_url }
▼
سرور ما
│ Withdraw (sts=1) می‌سازد
│ درگاه فعال را از Settings انتخاب می‌کند (مثلاً montify)
│ با HMAC به Montify: POST /external-payment/initiate
▼
Montify → payment_url → درگاه بانکی
│
├─► Browser return → GET /customer/wallet/payment/return/{provider}
└─► Webhook → POST /customer/wallet/payment/webhook/{provider}
│
└─► شارژ idempotent کیف (sts=2 + Transaction)
```
**منبع حقیقت شارژ:** webhook (یا verify امن سمت سرور). فقط به query برگشت مرورگر اعتماد نکنید؛ امضا را verify کنید و مبلغ/`reference_id` را با `Withdraw` تطبیق دهید.
---
## ۳. امنیت — قوانین اجباری
| درست | غلط |
|------|-----|
| `client_id` / `client_secret` / `base_url` فقط در Settings سرور | گذاشتن secret در JS فرانت |
| فیلتر `api/get_settings` برای کلیدهای محرمانه | برگرداندن `external_payment_*` به اپ کاربر |
| اپ فقط `online_enabled` و بعد `payment_url` می‌بیند | خواندن provider یا credential از settings در کلاینت |
| initiate فقط با JWT مشتری | ساخت لینک پرداخت در مرورگر |
| verify امضای return و webhook | اعتماد کور به query بدون signature |
| تطبیق `amount` با `Withdraw.cash` | قبول هر مبلغ برگشتی |
### ۳.۱. چه چیزی به اپ کاربر می‌رسد؟
- `GET /customer/wallet/payment/options` → فقط `{ "online_enabled": true|false }`
- `POST /customer/wallet/payment/initiate` → فقط `{ "payment_url": "https://..." }`
- **هیچ** `client_id`، `client_secret`، `base_url`، نام درگاه داخلی، یا `wallet_payment_provider` در Network تب اپ نباید دیده شود.
### ۳.۲. فیلتر `get_settings`
در `App\Models\Setting::isSecretOrServerOnlyKey()` و `SettingsController::authorizeSettingsPayload()`:
- درخواست عمومی (`api/get_settings` بدون auth ادمین) → کلیدهای محرمانه حذف می‌شوند.
- پنل ادمین (`admin/get_settings` با `auth:user`) → همه کلیدها قابل مشاهده/ویرایش طبق `can_edit`.
کلیدهای مرتبط با پرداخت که به اپ عمومی نمی‌روند شامل:
- همه `external_payment_*`
- `wallet_payment_provider`
- همه `payping_*`
- الگوهای `*_secret` / `*_token` / `*_password` و لیست ثابت توکن‌ها
---
## ۴. Settings (جدول `settings`)
پیکربندی فقط از پنل ادمین / دیپلوی. گروه UI: **درگاه خارجی (Montify)**.
| کلید | نوع | توضیح |
|------|-----|--------|
| `wallet_payment_provider` | select | `none` = خاموش، `montify` = پارتنر Montify. برای درگاه‌های بعدی همین فیلد گسترش می‌یابد. |
| `external_payment_enabled` | switch | سازگاری قدیمی؛ اگر `true` باشد و provider خالی باشد، مثل `montify` رفتار می‌کند. ترجیحاً از `wallet_payment_provider` استفاده کنید. |
| `external_payment_base_url` | text | BASE_URL API Montify (بدون اسلش انتهایی) |
| `external_payment_client_id` | text | `client_id` پارتنر |
| `external_payment_client_secret` | text | `client_secret` — محرمانه |
| `external_payment_gateway` | select | درگاه داخل Montify: `zarinpall` / `zibal` / `payping` / `sepal` |
| `external_payment_return_url` | text | اختیاری؛ خالی = پیش‌فرض سرور |
| `external_payment_webhook_url` | text | اختیاری؛ خالی = پیش‌فرض سرور |
| `min_deposit` | price | حداقل مبلغ واریز (مشترک با واریز دستی) |
### مقادیر پیش‌فرض URL وقتی فیلد خالی است
- Return: `{APP}/customer/wallet/payment/return/montify`
- Webhook: `{APP}/customer/wallet/payment/webhook/montify`
این آدرس‌ها باید در whitelist پارتنر Montify ثبت شوند.
### Migrationها
- `2026_07_24_000001_add_external_payment_settings.php`
- `2026_07_24_000002_add_wallet_payment_provider_setting.php`
```bash
php artisan migrate
```
---
## ۵. APIهای سرور ما
پیشوند: `/customer`
Auth مشتری: `auth:customer` + `customer.active` (به‌جز return/webhook)
| متد | مسیر | Auth | نقش |
|-----|------|------|-----|
| GET | `/wallet/payment/options` | مشتری | `{ online_enabled }` |
| POST | `/wallet/payment/initiate` | مشتری | Body: `{ value }` (ریال) → `{ payment_url }` |
| GET | `/wallet/payment/return/{provider}` | عمومی | برگشت مرورگر؛ verify + گزارش |
| POST | `/wallet/payment/webhook/{provider}` | عمومی (HMAC) | شارژ کیف |
### سازگاری قدیمی
| مسیر قدیمی | معادل |
|------------|--------|
| `POST /wallet/external-payment/initiate` | همان `initiate` |
| `GET /wallet/external-payment/return` | `return/montify` |
| `POST /wallet/external-payment/webhook` | `webhook/montify` |
### Initiate — جزئیات
1. بررسی `WalletPaymentManager::isOnlineDepositAvailable()`
2. اعتبارسنجی مبلغ (≥ ۱۰۰۰ و ≥ `min_deposit`)
3. ساخت `Withdraw` با `sts=1`، `note=واریز آنلاین`
4. `reference_id = WD-{withdraw_id}`
5. فراخوانی `gateway->initiate(...)`
6. ذخیره `session_id` در `tracked_number` در صورت وجود
7. پاسخ فقط `payment_url`
### Webhook / Return — شارژ
منطق مشترک در `App\Services\Payment\WalletDepositCreditor`:
- قفل ردیف `Withdraw` (idempotent)
- اگر قبلاً `sts=2` باشد، دوباره شارژ نمی‌کند
- `TransactionInterface` نوع واریز نقدی
- همگام‌سازی حسابداری (Pargar) در صورت امکان
- SMS ادمین با سوییچ `sms_notif_gateway_charge_admin`
---
## ۶. معماری کد (چنددرگاهی)
```
app/Services/Payment/
Contracts/
WalletPaymentGatewayInterface.php # قرارداد درگاه
Gateways/
MontifyPartnerGateway.php # پیاده‌سازی فعلی Montify
# PaypingDirectGateway.php # آینده
Support/
MontifyHmacSigner.php # HMAC + stableStringify
WalletPaymentManager.php # انتخاب درگاه از Settings
WalletDepositCreditor.php # شارژ idempotent کیف
app/Http/Controllers/pay/
WalletPaymentController.php # options / initiate / return / webhook
```
### قرارداد درگاه
هر درگاه باید این‌ها را پیاده کند:
- `key(): string` — مثلاً `montify`
- `isAvailable(): bool` — فعال + پیکربندی کامل
- `initiate(array $payload): array` — حداقل `payment_url`
- `returnUrl()` / `webhookUrl()`
- `handleReturn(Request): array`
- `handleWebhook(Request): array`
### افزودن درگاه جدید (چک‌لیست)
1. کلاس جدید در `Gateways/` با `WalletPaymentGatewayInterface`
2. ثبت در `WalletPaymentManager::drivers()`
3. افزودن مقدار به optionsی `wallet_payment_provider` در migration/admin (`settingTypes.ts`)
4. Settings مخصوص آن درگاه (و اضافه کردن به فیلتر secret در `Setting::isSecretOrServerOnlyKey`)
5. مسیرهای `return/{provider}` و `webhook/{provider}` از قبل generic هستند
6. تست: options → initiate → webhook → موجودی کیف
---
## ۷. فرانت‌اند (khaneghi_user)
| فایل | نقش |
|------|-----|
| `src/api/wallet.ts` | `getWalletPaymentOptions` / `initiateWalletPayment` |
| `src/views/base/wallet/composables/useWalletPage.ts` | بارگذاری `online_enabled`، `payOnline()` |
| `src/views/base/wallet/components/WalletDepositGuideModal.vue` | دکمه «پرداخت آنلاین از درگاه» |
| `src/views/base/wallet/index.vue` | اتصال props/events مدال |
رفتار UI:
1. کاربر مبلغ را وارد می‌کند و واریز می‌زند → مدال راهنما (کارت/شبا + پشتیبانی)
2. اگر `online_enabled` باشد دکمه درگاه نمایش داده می‌شود
3. کلیک → `initiate` → `window.location = payment_url`
**نکته:** وضعیت دکمه از endpoint سرور می‌آید، نه از `settingsStore` و کلیدهای `external_payment_*`.
---
## ۸. پنل ادمین
- گروه تنظیمات: `admin/.../configuration/settingGroups.ts` → id `external_payment`
- انواع/برچسب/قفل: `settingTypes.ts`
- ویرایش credentialها فقط در ادمین (و کلیدهای `can_edit=0` فقط دیپلوی)
---
## ۹. مدل داده مرتبط
| مدل | نقش |
|-----|-----|
| `Withdraw` | درخواست واریز؛ `sts`: 1=در انتظار، 2=موفق، 3=ناموفق |
| `reference_id` | `WD-{id}` برای Montify |
| Wallet `TransactionInterface` | واریز نقدی پس از موفقیت |
واریز دستی تغییری نکرده؛ فقط راهنما + کپی کارت/شبا از Settings عمومی (`deposit_card_number` و …) است.
---
## ۱۰. راه‌اندازی عملیاتی
1. `php artisan migrate`
2. در پنل ادمین / دیپلوی:
- `wallet_payment_provider` = `montify`
- `external_payment_base_url` / `client_id` / `client_secret` / `gateway`
3. در پنل Montify:
- ثبت `allowed_return_urls` و `webhook_url` مطابق URLهای سرور
4. تست با مبلغ ≥ `min_deposit`
5. تأیید: Network اپ کاربر فاقد secret باشد؛ فقط `options` و `initiate` دیده شود
---
## ۱۱. عیب‌یابی سریع
| مشکل | بررسی |
|------|--------|
| دکمه درگاه نیست | `options.online_enabled`؛ provider؛ کامل بودن credential؛ `isAvailable()` |
| `مشکل در ارتباط با درگاه` | ساعت سرور (NTP)، `stableStringify`، PATH دقیق، body خام همان hash |
| شارژ دوبل | باید idempotent باشد (`sts=2` + lock) |
| secret در Network | مسیر باید `api/get_settings` باشد نه admin؛ فیلتر `Setting::forPublicApi` |
جزئیات امضای HMAC و خطاهای Montify: [`EXTERNAL_PAYMENT_PARTNER.md`](./EXTERNAL_PAYMENT_PARTNER.md)
---
## ۱۲. ارجاع فایل‌های کلیدی
```
server/docs/WALLET_ONLINE_PAYMENT.md ← این سند
server/docs/EXTERNAL_PAYMENT_PARTNER.md ← پروتکل Montify
server/app/Services/Payment/...
server/app/Http/Controllers/pay/WalletPaymentController.php
server/routes/customer.php
server/app/Models/Setting.php ← فیلتر secret
server/database/migrations/2026_07_24_000001_add_external_payment_settings.php
server/database/migrations/2026_07_24_000002_add_wallet_payment_provider_setting.php
khaneghi_user/src/api/wallet.ts
khaneghi_user/src/views/base/wallet/...
admin/src/pages/menuPages/configuration/settingTypes.ts
admin/src/pages/menuPages/configuration/settingGroups.ts
```