# پرداخت و برداشت

## اصل‌ها

1. **Callback هیچ‌وقت به‌تنهایی پرداخت را تأیید نمی‌کند.** هر Callback فقط باعث می‌شود وضعیت از خود درگاه / زنجیره استعلام شود (`PaymentGatewayInterface::verify`).
2. **مبلغ همیشه از دیتابیس خودمان خوانده می‌شود**، نه از درخواست.
3. **تأیید Idempotent است**: `PaymentService::confirm()` ردیف پرداخت را قفل می‌کند، فقط `pending → paid` را می‌پذیرد و Ledger را با کلید `payment:{id}` می‌نویسد. Callback تکراری، Retry Worker یا تأیید هم‌زمان چند پردازه ← یک تراکنش.
4. **همه‌ی پول از Ledger می‌گذرد**: پرداخت درگاه ← `deposit` در کیف پول ← (اگر برای سفارش است) `campaign_expense` از کیف پول.
5. واریز درگاه به‌صورت پیش‌فرض **قابل خرج ولی غیرقابل برداشت** است (`wallet.deposits_withdrawable`).

## وضعیت‌های پرداخت

```text
pending ──verify: paid──► paid          (نهایی)
   │──────verify: failed─► failed       (نهایی)
   └──────مهلت تمام شد──► expired       (اگر بعداً روی زنجیره دیده شود، همچنان قابل تأیید است)
```

هر رویداد در `payment_transactions` ثبت می‌شود (`created`, `callback`, `callback_rejected`, `hash_submitted`, `confirmed`, `failed`, `expired`) — بدون Secret.

## درگاه‌ها

| کلید | کلاس | نوع | Feature Flag | تأیید |
|---|---|---|---|---|
| `zarinpal` | `ZarinPalGateway` | ریالی، Redirect | `zarinpal` | `verify.json` با Authority و مبلغ ما (کد ۱۰۰ یا ۱۰۱) |
| `nowpayments` | `NowPaymentsGateway` | رمزارز، Invoice | `nowpayments` + `crypto` | امضای HMAC-SHA512 روی IPN **و** استعلام `GET /payment/{id}` |
| `payment4` | `Payment4Gateway` | رمزارز، Redirect | `payment4` + `crypto` | `PUT /payment/verify` با `paymentUid` و مبلغ ما |
| `tron` | `TronGateway` | TRX مستقیم | `tron` + `crypto` | TronGrid: مقصد، مبلغ، موفقیت، تعداد تأیید |
| `ton` | `TonGateway` | TON مستقیم | `ton` + `crypto` | TON Center: Memo یکتا، مبلغ، زمان |

درگاهی که کلیدش در `.env` خالی باشد در ربات نمایش داده نمی‌شود (`isConfigured()`).

> آدرس و قالب API سرویس‌های ثالث (NowPayments، Payment4، TronGrid، TON Center، Nobitex) طبق مستندات عمومی آن‌ها پیاده شده و در تست‌ها با پاسخ شبیه‌سازی‌شده بررسی شده است. قبل از فعال‌سازی هر درگاه، یک پرداخت واقعیِ کم‌مبلغ (یا Sandbox) انجام دهید و `storage/logs/payment-*.log` را ببینید.

### ZarinPal

```text
create:  POST /pg/v4/payment/request.json {merchant_id, amount, currency: IRT, callback_url}
         → authority → کاربر: /pg/StartPay/{authority}
callback: GET /payment/callback/zarinpal?token=…&Authority=…&Status=OK|NOK
verify:  Authority باید همان Authority ثبت‌شده باشد (وگرنه failed)
         POST /pg/v4/payment/verify.json {merchant_id, amount (از DB), authority}
```

اگر کاربر پرداخت کند ولی به صفحه‌ی بازگشت نرسد، `PaymentMaintenanceJob` قبل از منقضی‌کردن یک‌بار دیگر Verify می‌کند. مدیر هم می‌تواند از پنل «بررسی مجدد از درگاه» را بزند.

### TRON

- نرخ: Nobitex (با Cache) یا `rate.trx`.
- TRX حافظه (Memo) ندارد؛ پس به هر پرداخت **مبلغ یکتا** داده می‌شود (گرد به بالا تا ۰٫۰۱ TRX + دنباله‌ی تصادفی تا ۹۹۹۹ SUN که بین پرداخت‌های باز تکراری نیست).
- `VerifyPaymentJob` هر ۳۰ ثانیه و `PaymentMaintenanceJob` هر دقیقه تراکنش‌های ورودی آدرس خزانه را می‌خواند و دنبال `TransferContract` موفق با **دقیقاً** همان مبلغ می‌گردد.
- کاربر می‌تواند TX Hash بفرستد؛ این فقط «سرنخ» است. همان تراکنش با همان قواعد بررسی می‌شود: مقصد = آدرس ما، مبلغ ≥ مورد انتظار، `contractRet = SUCCESS`، زمان تراکنش بعد از ایجاد پرداخت، تأییدها ≥ `TRON_MIN_CONFIRMATIONS`، و هش قبلاً استفاده نشده باشد.
- هش اشتباه پرداخت را خراب نمی‌کند؛ سیستم به اسکن ادامه می‌دهد.

### TON

- هر پرداخت یک **Memo یکتا** دارد؛ تنها تراکنشی پذیرفته می‌شود که Comment آن دقیقاً همان Memo باشد، مبلغش ≥ مورد انتظار و بعد از ایجاد پرداخت ثبت شده باشد.
- لینک `ton://transfer/…?amount=…&text=…` برای کیف پول‌ها ساخته می‌شود.

## افزودن درگاه جدید

1. کلاسی با `PaymentGatewayInterface` بسازید (`key`, `isCrypto`, `isConfigured`, `create`, `authenticate`, `verify`). فقط از `HttpClientInterface` برای HTTP استفاده کنید.
2. در `config/payments.php → gateways` ثبت کنید و یک Feature Flag در `config/settings.php` اضافه کنید.
3. متن `gateway.{key}` را به `resources/messages/fa.php` اضافه کنید.

هیچ تغییری در `PaymentService`، ربات یا پنل لازم نیست. افزودن ارز/شبکه‌ی جدید (مثلاً USDT-TRC20) هم همین مسیر است: یک Gateway جدید که `currency` و `network` خودش را در `PaymentIntent` برمی‌گرداند.

## برداشت

```text
create  → قفل کیف پول → بررسی قواعد → debit از «قابل برداشت» (Ledger: withdraw) → withdrawals: pending
approve → approved → processing → completed (TX Hash / کد پیگیری)
reject | cancel (کاربر، فقط pending) → Ledger: refund (یک‌بار، کلید withdrawal:{id}:refund)
```

- مبلغ **در لحظه‌ی ثبت درخواست** از کیف پول خارج می‌شود؛ بنابراین دو درخواست هم‌زمان برای یک موجودی نمی‌توانند هر دو موفق شوند (تست: `ConcurrencyTest`).
- پس از `completed` دیگر رد یا Refund ممکن نیست.
- قواعد (تنظیمات): `withdrawal.min/max`, `fee_percent`, `fee_fixed`, `daily_count`, `require_kyc`, `pending_ttl_days`.
- مقصد (کارت / شبا / آدرس) رمزشده ذخیره می‌شود؛ در فهرست‌ها و لاگ فقط نسخه‌ی Mask شده دیده می‌شود. نمایش کامل مقصد فقط برای مدیرِ دارای `withdrawals.approve` و با ثبت در Audit Log.
- اعتبارسنجی مقصد: کارت (Luhn)، شبا (mod 97)، TRON (Base58Check)، TON (CRC16).

پرداخت برداشت‌ها دستی است (مدیر پس از واریز، TX Hash را ثبت می‌کند). برای پرداخت خودکار، یک Job بنویسید که برداشت‌های `approved` را بردارد، از طریق Adapter شبکه واریز کند و `WithdrawalService::complete()` را صدا بزند — چرخه‌ی وضعیت و Idempotency آماده است.

### افزودن روش برداشت

کلاسی با `WithdrawalMethodInterface` (`key`, `feature`, `normalize`, `mask`) بسازید، در `config/payments.php → withdrawal_methods` ثبت کنید و متن `withdrawal.method.{key}` را اضافه کنید.

## کمیسیون‌ها

| نوع | تنظیم | چه زمانی | به چه کسی |
|---|---|---|---|
| کارمزد پلتفرم | `commission.campaign_percent` | هنگام ساخت کمپین (`publisher_price = unit_price − کارمزد`) | پلتفرم |
| Referral چندسطحی | `referral.levels` | هنگام تسویه‌ی درآمد ناشر | سرشاخه‌های ناشر |
| Sales | `commission.sales_percent` | پرداخت سفارش | معرف مستقیم خریدار |
| Partner | `commission.partner_percent` | تکمیل کمپین (روی بودجه‌ی مصرف‌شده) | معرف مستقیم تبلیغ‌دهنده |

همه با کلید Idempotency در Ledger ثبت می‌شوند و فقط یک‌بار پرداخت می‌شوند.
