Files

287 lines
10 KiB
PHP

<?php
namespace App\Http\Controllers\pay;
use App\Http\Controllers\Controller;
use App\Models\Withdraw;
use App\Services\ConfigService;
use App\Services\Payment\Contracts\SupportsStatusQuery;
use App\Services\Payment\WalletDepositCreditor;
use App\Services\Payment\WalletPaymentManager;
use Illuminate\Http\Request;
use InvalidArgumentException;
use RuntimeException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
/**
* API عمومی شارژ آنلاین کیف — بدون افشای credential به کلاینت.
* انتخاب درگاه فقط سمت سرور (wallet_payment_provider).
*/
class WalletPaymentController extends Controller
{
public function __construct(
private WalletPaymentManager $payments,
private ConfigService $config,
private WalletDepositCreditor $creditor,
) {
}
/**
* فقط فلگ نمایش دکمه — بدون نام درگاه و بدون تنظیمات محرمانه.
*/
public function options()
{
return response()->json($this->payments->publicOptions());
}
public function initiate(Request $request)
{
$gateway = $this->payments->resolve();
if (!$gateway || !$gateway->isAvailable()) {
throw new UnprocessableEntityHttpException('پرداخت آنلاین در حال حاضر فعال نیست');
}
$amount = (int) $request->input('value', $request->input('amount', 0));
$minDeposit = $this->config->getInt('min_deposit', 0);
if ($amount < 1000) {
throw new UnprocessableEntityHttpException('حداقل مبلغ پرداخت ۱۰۰۰ ریال است');
}
if ($minDeposit > 0 && $amount < $minDeposit) {
throw new UnprocessableEntityHttpException(
'حداقل مقدار واریز ' . number_format($minDeposit) . ' ریال است'
);
}
$customer = auth('customer')->user();
self::assertCustomerIdentityVerified($customer instanceof \App\Models\Customer ? $customer : null);
$withdraw = Withdraw::create([
'customer_id' => $customer->id,
'cash' => $amount,
'note' => 'واریز آنلاین',
'sts' => 1,
]);
$referenceId = 'WD-' . $withdraw->id;
try {
$result = $gateway->initiate([
'amount' => $amount,
'reference_id' => $referenceId,
'description' => 'شارژ کیف پول کاربر ' . ($customer->name ?? $customer->id),
'metadata' => [
'withdraw_id' => $withdraw->id,
'customer_id' => $customer->id,
],
]);
} catch (RuntimeException $e) {
$withdraw->sts = 3;
$withdraw->save();
\Log::warning('wallet.payment.initiate_failed', [
'withdraw_id' => $withdraw->id,
'message' => $e->getMessage(),
]);
return response()->json([
'message' => $e->getMessage() ?: 'مشکل در ارتباط با درگاه',
], 422);
}
if (!empty($result['session_id'])) {
$withdraw->tracked_number = (string) $result['session_id'];
$withdraw->save();
}
// حداقل اطلاعات برای redirect — بدون credential و بدون جزئیات داخلی درگاه
return response()->json([
'payment_url' => $result['payment_url'],
], 201);
}
/**
* وریفای برگشت مرورگر از فرانت — امضا و شارژ سمت سرور؛ بدون افشای secret.
* Body یا query: status, reference_id, session_id, amount, track_number, timestamp, signature
*/
public function verifyReturn(Request $request)
{
$gateway = $this->payments->resolve();
if (!$gateway) {
throw new UnprocessableEntityHttpException('پرداخت آنلاین فعال نیست');
}
// همه پارامترهای برگشت عیناً وارد امضا می‌شوند؛ whitelist کردن باعث
// مغایرت امضا می‌شود اگر پارتنر پارامتر اضافه‌ای بفرستد.
$payload = [];
foreach ($request->all() as $key => $value) {
if (!is_string($key) || $key === '' || is_array($value) || $value === null) {
continue;
}
if (count($payload) >= 30) {
break;
}
$payload[$key] = is_bool($value) ? ($value ? 'true' : 'false') : (string) $value;
}
if (($payload['signature'] ?? '') === '' || ($payload['reference_id'] ?? '') === '') {
return response()->json([
'ok' => false,
'message' => 'پارامترهای بازگشت ناقص است',
], 422);
}
$result = $gateway->handleReturnPayload($payload);
if (!($result['ok'] ?? false)) {
// redirect مرورگر قابل اعتماد نیست؛ منبع حقیقت، استعلام مستقیم از درگاه است.
$fallback = $this->statusFallback((string) $payload['reference_id']);
if ($fallback) {
return $fallback;
}
return response()->json([
'ok' => false,
'message' => $result['message'] ?? 'خطا در تأیید پرداخت',
], (int) ($result['http_status'] ?? 400));
}
/** @var Withdraw|null $withdraw */
$withdraw = $result['withdraw'] ?? null;
if (!$withdraw) {
return response()->json(['ok' => false, 'message' => 'سفارش یافت نشد'], 404);
}
if (!$this->ownsWithdraw($withdraw)) {
return response()->json(['ok' => false, 'message' => 'دسترسی غیرمجاز'], 403);
}
$paid = (int) $withdraw->sts === 2;
return $this->paymentStateResponse(
$withdraw,
$paid ? 'success' : ((string) ($payload['status'] ?? '') === 'success' ? 'pending' : 'failed')
);
}
/**
* استعلام وضعیت یک پرداخت از درگاه — برای polling فرانت وقتی نتیجه هنوز قطعی نیست.
*/
public function status(Request $request)
{
$referenceId = trim((string) $request->input('reference_id', ''));
if ($referenceId === '') {
return response()->json(['ok' => false, 'message' => 'شناسه پرداخت ارسال نشده'], 422);
}
$response = $this->statusFallback($referenceId);
if ($response) {
return $response;
}
return response()->json(['ok' => false, 'message' => 'سفارش یافت نشد'], 404);
}
/**
* استعلام مستقیم وضعیت از درگاه با بررسی مالکیت سفارش.
*/
private function statusFallback(string $referenceId): ?\Illuminate\Http\JsonResponse
{
$withdraw = $this->creditor->findByReference($referenceId);
if (!$withdraw || !$this->ownsWithdraw($withdraw)) {
return null;
}
if ((int) $withdraw->sts !== 2) {
$gateway = $this->payments->resolve();
if ($gateway instanceof SupportsStatusQuery) {
$gateway->syncStatusByReference($referenceId);
$withdraw = $withdraw->fresh() ?: $withdraw;
}
}
$sts = (int) $withdraw->sts;
return $this->paymentStateResponse(
$withdraw,
match ($sts) {
2 => 'success',
3 => 'failed',
default => 'pending',
}
);
}
private function ownsWithdraw(Withdraw $withdraw): bool
{
return (int) $withdraw->customer_id === (int) auth('customer')->id();
}
private function paymentStateResponse(Withdraw $withdraw, string $status): \Illuminate\Http\JsonResponse
{
return response()->json([
'ok' => true,
'paid' => (int) $withdraw->sts === 2,
'status' => $status,
'amount' => (int) $withdraw->cash,
'reference_id' => 'WD-' . $withdraw->id,
'withdraw_id' => $withdraw->id,
'track_number' => $withdraw->tracked_number,
]);
}
public function returnFromGateway(Request $request, string $provider)
{
try {
$gateway = $this->payments->resolveOrFail($provider);
} catch (InvalidArgumentException $e) {
return response('درگاه نامعتبر', 404);
}
$result = $gateway->handleReturn($request);
if (!($result['ok'] ?? false)) {
return response(
(string) ($result['message'] ?? 'خطا'),
(int) ($result['http_status'] ?? 400)
);
}
$Withdraw = $result['withdraw'];
$site_url = $this->config->resolvePublicSiteUrl($request) ?: '/';
return view('showTransactionReport', compact('Withdraw', 'site_url'));
}
public function webhook(Request $request, string $provider)
{
try {
$gateway = $this->payments->resolveOrFail($provider);
} catch (InvalidArgumentException $e) {
return response()->json(['message' => 'invalid_provider'], 404);
}
$result = $gateway->handleWebhook($request);
$status = (int) ($result['http_status'] ?? 200);
if (!($result['ok'] ?? false)) {
return response()->json([
'message' => $result['message'] ?? 'error',
], $status >= 400 ? $status : 400);
}
return response()->json([
'ok' => true,
'ignored' => (bool) ($result['ignored'] ?? false),
]);
}
/** سازگاری با URLهای قبلی external-payment/* → montify */
public function returnLegacy(Request $request)
{
return $this->returnFromGateway($request, WalletPaymentManager::PROVIDER_MONTIFY);
}
public function webhookLegacy(Request $request)
{
return $this->webhook($request, WalletPaymentManager::PROVIDER_MONTIFY);
}
}