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}&currency=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) . "&note=" . 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());
}

Выполнить команду


Для локальной разработки. Не используйте в интернете!