PHP WebShell
Текущая директория: /var/www/bitcardoApp/models/fiat
Просмотр файла: process_send_fiat.php
<?php
// models/fiat/process_send_fiat.php
require_once '../../config/db_config.php';
session_start();
function fail_redirect($msg) {
header("Location: ../../user/fiat/send_fiat_failed.php?error=" . urlencode($msg));
exit;
}
if (!isset($_SESSION["user_id"])) fail_redirect("Session expired. Please login again.");
$userId = (int)($_SESSION["user_id"]);
$walletId = (int)($_POST["wallet_id"] ?? 0);
$bankCode = trim((string)($_POST["bank_code"] ?? ""));
$accountNumber = trim((string)($_POST["account_number"] ?? ""));
$accountName = trim((string)($_POST["account_name"] ?? ""));
$amountNaira = (float)($_POST["amount"] ?? 0);
$reason = trim((string)($_POST["reason"] ?? ""));
if (!$walletId || $bankCode === "" || $accountNumber === "" || $amountNaira <= 0) fail_redirect("Invalid input data.");
if (!preg_match('/^\d{10}$/', $accountNumber)) fail_redirect("Account number must be 10 digits.");
if (!preg_match('/^\d+$/', $bankCode)) fail_redirect("Invalid bank code.");
if ($amountNaira < 100) fail_redirect("Minimum transfer is ₦100.");
if (empty($paystackSecret)) fail_redirect("Server misconfiguration: Paystack secret key not set.");
/* setting: add_paystack_naira_withdraw_fee */
function get_setting(mysqli $conn, string $key, $default = null) {
$stmt = $conn->prepare("SELECT setting_value FROM site_settings WHERE setting_key = ? LIMIT 1");
if (!$stmt) return $default;
$stmt->bind_param("s", $key);
$stmt->execute();
$stmt->bind_result($val);
$ok = $stmt->fetch();
$stmt->close();
return $ok ? $val : $default;
}
$addWithdrawFee = get_setting($conn, 'add_paystack_naira_withdraw_fee', '1') === '1';
/* Paystack HTTP helper */
function paystack_request($method, $url, $secret, $payload = null) {
$ch = curl_init();
$headers = [
"Authorization: Bearer {$secret}",
"Accept: application/json",
];
if ($payload !== null) $headers[] = "Content-Type: application/json";
$opts = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 30,
];
$method = strtoupper($method);
if ($method === 'POST') $opts[CURLOPT_POST] = 1;
if ($payload !== null) $opts[CURLOPT_POSTFIELDS] = json_encode($payload);
curl_setopt_array($ch, $opts);
$raw = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [$code, $raw, $err];
}
try {
$conn->begin_transaction();
// 1) Lock wallet & verify
$stmt = $conn->prepare("
SELECT balance, wallet_add
FROM user_wallets
WHERE wallet_id = ? AND user_id = ? AND type = 'fiat' AND coin = 'NGN'
FOR UPDATE
");
$stmt->bind_param("ii", $walletId, $userId);
$stmt->execute();
$rs = $stmt->get_result();
if ($rs->num_rows === 0) throw new Exception("Wallet not found or unauthorized.");
$w = $rs->fetch_assoc();
$currentBalance = (float)$w["balance"];
$senderAddress = (string)($w["wallet_add"] ?? "");
$stmt->close();
// 2) Resolve account (server-side safety)
$resolveUrl = "https://api.paystack.co/bank/resolve?account_number=" . urlencode($accountNumber) . "&bank_code=" . urlencode($bankCode);
[$codeR, $rawR, $errR] = paystack_request('GET', $resolveUrl, $paystackSecret);
if ($errR) throw new Exception("Error resolving account (network).");
$respR = json_decode($rawR, true);
if ($codeR !== 200 || empty($respR['status'])) {
error_log("[Paystack Resolve] HTTP={$codeR} raw={$rawR}");
throw new Exception("Failed to resolve account details.");
}
$resolvedName = $respR['data']['account_name'] ?? '';
if ($resolvedName === '') throw new Exception("Account could not be resolved.");
// 3) Determine Paystack fee
$amountKobo = (int)round($amountNaira * 100);
$feeNaira = null;
$feeUrl = "https://api.paystack.co/transfer/fee?amount={$amountKobo}¤cy=NGN";
[$codeF, $rawF, $errF] = paystack_request('GET', $feeUrl, $paystackSecret);
if (!$errF) {
$respF = json_decode($rawF, true);
if ($codeF === 200 && !empty($respF['status']) && isset($respF['data']['fee'])) {
$feeNaira = ((int)$respF['data']['fee']) / 100.0;
} else {
error_log("[Paystack Fee] HTTP={$codeF} raw={$rawF} — fallback tiers");
}
} else {
error_log("[Paystack Fee] network error: {$errF} — fallback tiers");
}
if ($feeNaira === null) {
if ($amountNaira <= 5000) $feeNaira = 10.0;
elseif ($amountNaira <= 50000) $feeNaira = 25.0;
else $feeNaira = 50.0;
}
// 4) Balance check (includes fee when toggle is on)
$totalDebit = $addWithdrawFee ? ($amountNaira + $feeNaira) : $amountNaira;
if ($totalDebit > $currentBalance) {
throw new Exception("Insufficient wallet balance" . ($addWithdrawFee ? " (amount + Paystack fee)" : "") . ".");
}
// 5) Create Paystack recipient
$recipientPayload = [
"type" => "nuban",
"name" => $resolvedName,
"account_number" => $accountNumber,
"bank_code" => (string)$bankCode,
"currency" => "NGN"
];
[$codeRc, $rawRc, $errRc] = paystack_request('POST', "https://api.paystack.co/transferrecipient", $paystackSecret, $recipientPayload);
if ($errRc) throw new Exception("Network error creating recipient.");
$respRc = json_decode($rawRc, true);
if ($codeRc < 200 || $codeRc >= 300 || empty($respRc['status'])) {
error_log("[Paystack Recipient] HTTP={$codeRc} raw={$rawRc}");
throw new Exception($respRc['message'] ?? "Failed to create transfer recipient.");
}
$recipientCode = $respRc['data']['recipient_code'] ?? null;
$bankNameApi = $respRc['data']['details']['bank_name'] ?? '';
if (!$recipientCode) throw new Exception("Missing recipient code from Paystack.");
// 6) Initiate Paystack transfer
$reference = "BDO-".date('YmdHis')."-U{$userId}-W{$walletId}-".bin2hex(random_bytes(4));
$transferPayload = [
"source" => "balance",
"amount" => $amountKobo,
"recipient" => $recipientCode,
"reason" => $reason ?: "User withdrawal",
"reference" => $reference
];
[$codeTx, $rawTx, $errTx] = paystack_request('POST', "https://api.paystack.co/transfer", $paystackSecret, $transferPayload);
if ($errTx) throw new Exception("Network error initiating transfer.");
$respTx = json_decode($rawTx, true);
if ($codeTx < 200 || $codeTx >= 300 || empty($respTx['status'])) {
error_log("[Paystack Transfer] HTTP={$codeTx} raw={$rawTx}");
throw new Exception($respTx['message'] ?? "Failed to initiate transfer.");
}
$psData = $respTx['data'] ?? [];
$providerRef = $psData['reference'] ?? $reference;
$transferStatus = $psData['status'] ?? 'pending'; // e.g. pending/otp
$transferCode = $psData['transfer_code'] ?? null;
$message = $respTx['message'] ?? '';
$otpRequired = (stripos($message, 'otp') !== false) || (stripos((string)$transferStatus, 'otp') !== false);
// 7) Ledger rows (transactions table only)
$providerMeta = [
'bank_code' => $bankCode,
'bank_name' => $bankNameApi,
'account_name' => $resolvedName,
'account_number' => $accountNumber,
'transfer_amount' => $amountNaira,
'paystack_fee' => $feeNaira,
'policy_add_fee' => $addWithdrawFee ? 1 : 0,
'recipient_code' => $recipientCode,
'transfer_code' => $transferCode,
'provider_ref' => $providerRef,
'raw_initiate' => $psData,
];
$metaJson = json_encode($providerMeta, JSON_UNESCAPED_SLASHES);
// main withdrawal row
$insX = $conn->prepare("
INSERT INTO transactions
(coin, user_id, wallet_id, sender_address, receiver_address, amount, type, txid, reference, provider, provider_meta, status, applied, note, updated_at, created_at)
VALUES ('NGN', ?, ?, ?, ?, ?, 'withdrawal', '', ?, 'paystack', ?, ?, 0, 'Paystack transfer to bank', NOW(), NOW())
");
$statusSave = $otpRequired ? 'otp_required' : 'pending';
$insX->bind_param("iissdsss", $userId, $walletId, $senderAddress, $accountNumber, $amountNaira, $providerRef, $metaJson, $statusSave);
$insX->execute();
$insX->close();
// optional fee row only if we *add* the fee to user's debit
if ($addWithdrawFee && $feeNaira > 0) {
$insF = $conn->prepare("
INSERT INTO transactions
(coin, user_id, wallet_id, sender_address, receiver_address, amount, type, txid, reference, provider, provider_meta, status, applied, note, updated_at, created_at)
VALUES ('NGN', ?, ?, ?, ?, ?, 'fee', '', ?, 'paystack', ?, 'pending', 0, 'Paystack transfer fee', NOW(), NOW())
");
$insF->bind_param("iissdss", $userId, $walletId, $senderAddress, $accountNumber, $feeNaira, $providerRef, $metaJson);
$insF->execute();
$insF->close();
}
// 8) Debit wallet (reserve)
$newBalance = $currentBalance - $totalDebit;
$updW = $conn->prepare("UPDATE user_wallets SET balance = ?, updated_at = NOW() WHERE wallet_id = ? LIMIT 1");
$updW->bind_param("di", $newBalance, $walletId);
$updW->execute();
$updW->close();
$conn->commit();
// 9) Redirect
if ($otpRequired) {
header("Location: ../../user/fiat/send_fiat_success.php?reference=" . urlencode($providerRef) . "¬e=" . urlencode("Transfer requires OTP to finalize"));
exit;
}
header("Location: ../../user/fiat/send_fiat_success.php?reference=" . urlencode($providerRef));
exit;
} catch (Throwable $e) {
try { $conn->rollback(); } catch (Throwable $__) {}
error_log("[WITHDRAW ERROR] user={$userId} wallet={$walletId} bank_code={$bankCode} acct={$accountNumber} msg=" . $e->getMessage());
fail_redirect($e->getMessage());
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!