PHP WebShell

Текущая директория: /var/www/bitcardoApp/models/crypto

Просмотр файла: buy_crypto.php

<?php
// models/crypto/buy_crypto.php
if (session_status() === PHP_SESSION_NONE) session_start();
require_once __DIR__ . '/../../config/db_config.php'; // expects $conn, $paystackSecret, $paystackPublic

if (!isset($_SESSION['user_id'])) {
    if (isset($_GET['action']) || isset($_POST['action'])) {
        header('Content-Type: application/json');
        echo json_encode(['ok' => false, 'msg' => 'Unauthorized']);
        exit;
    }
    header("Location: ../auth/login.php");
    exit;
}

$user_id = (int)$_SESSION['user_id'];

/* ---------------- Helpers ---------------- */

function json_out(array $payload, int $code = 200): void {
    http_response_code($code);
    header('Content-Type: application/json');
    echo json_encode($payload);
    exit;
}

function get_user_profile_limits(mysqli $conn, int $user_id): array {
    $sql = "
        SELECT u.email,
               ul.buy_limit,
               ul.instant_buy_limit
        FROM users u
        LEFT JOIN user_level ul ON ul.level_id = u.level_id
        WHERE u.user_id = ?
        LIMIT 1
    ";
    $stmt = $conn->prepare($sql);
    if (!$stmt) return ['email' => null, 'buy_limit' => null, 'instant_buy_limit' => null];

    $stmt->bind_param("i", $user_id);
    $stmt->execute();
    $stmt->bind_result($email, $buy_limit, $instant_buy_limit);

    $out = ['email' => null, 'buy_limit' => null, 'instant_buy_limit' => null];
    if ($stmt->fetch()) {
        $out['email'] = $email ?: null;

        $bl = (float)$buy_limit;
        $out['buy_limit'] = ($bl > 0) ? $bl : null;

        $ibl = (float)$instant_buy_limit;
        $out['instant_buy_limit'] = ($ibl > 0) ? $ibl : null;
    }
    $stmt->close();
    return $out;
}

function get_wallet_coin_for_user(mysqli $conn, int $user_id, string $wallet_id): ?string {
    $stmt = $conn->prepare("
        SELECT coin
        FROM user_wallets
        WHERE user_id=? AND wallet_id=? AND UPPER(coin) <> 'NGN'
        LIMIT 1
    ");
    if (!$stmt) return null;

    $stmt->bind_param("is", $user_id, $wallet_id);
    $stmt->execute();
    $stmt->bind_result($coin_db);

    $coin = null;
    if ($stmt->fetch()) {
        $coin = strtoupper(trim((string)$coin_db));

        // normalize variants to match coin_rates.coin
        if ($coin === 'USDT-TRC20' || $coin === 'USDT-ERC20') $coin = 'USDT';
        if ($coin === 'USDC-TRC20' || $coin === 'USDC-ERC20') $coin = 'USDC';
        if ($coin === 'TRON') $coin = 'TRX';
    }
    $stmt->close();
    return $coin;
}

function get_wallet_address_for_user(mysqli $conn, int $user_id, string $wallet_id): ?string {
    // uses user_wallets.wallet_add (your schema)
    $stmt = $conn->prepare("
        SELECT wallet_add
        FROM user_wallets
        WHERE user_id=? AND wallet_id=?
        LIMIT 1
    ");
    if (!$stmt) return null;

    $stmt->bind_param("is", $user_id, $wallet_id);
    $stmt->execute();
    $stmt->bind_result($wallet_add);

    $out = null;
    if ($stmt->fetch()) {
        $out = trim((string)$wallet_add);
        if ($out === '') $out = null;
    }
    $stmt->close();
    return $out;
}

function get_coin_buy_pricing(mysqli $conn, string $coin): array {
    $coin = strtoupper(trim($coin));

    $stmt = $conn->prepare("
        SELECT buy_rate, usd_price, min_swap
        FROM coin_rates
        WHERE UPPER(coin)=UPPER(?)
        LIMIT 1
    ");
    if (!$stmt) {
        return ['buy_rate' => null, 'usd_price' => null, 'min_swap' => 0.0];
    }

    $stmt->bind_param("s", $coin);
    $stmt->execute();
    $stmt->bind_result($buy_rate, $usd_price, $min_swap);

    $out = ['buy_rate' => null, 'usd_price' => null, 'min_swap' => 0.0];
    if ($stmt->fetch()) {
        $br = (float)$buy_rate;
        $up = (float)$usd_price;
        $ms = (float)$min_swap;

        $out['buy_rate']  = ($br > 0) ? $br : null;   // NGN per $1
        $out['usd_price'] = ($up > 0) ? $up : null;   // USD per 1 coin
        $out['min_swap']  = ($ms > 0) ? $ms : 0.0;    // USD
    }
    $stmt->close();
    return $out;
}

/**
 * Paystack fee estimate (NG):
 *  1.5% of amount
 *  +100 NGN if amount > 2500
 *  cap at 2000 NGN
 */
function estimate_paystack_fee(float $amountNgn): float {
    $pct = 0.015;
    $extra = ($amountNgn > 2500) ? 100 : 0;
    $cap = 2000;

    $fee = ($amountNgn * $pct) + $extra;
    if ($fee > $cap) $fee = $cap;
    return round($fee, 2);
}

/**
 * Gross-up so wallet receives netAmount, user pays fee on top.
 */
function gross_up_for_fee(float $netAmount): float {
    $gross = $netAmount;
    for ($i = 0; $i < 6; $i++) {
        $fee = estimate_paystack_fee($gross);
        $newGross = $netAmount + $fee;
        if (abs($newGross - $gross) < 0.5) {
            return round($newGross, 2);
        }
        $gross = $newGross;
    }
    return round($gross, 2);
}

function compute_quote(mysqli $conn, int $user_id, string $wallet_id, float $amount_usd): array {
    $wallet_id = trim($wallet_id);
    if ($wallet_id === '' || !($amount_usd > 0)) {
        return ['ok' => true, 'ready' => false];
    }

    $coin = get_wallet_coin_for_user($conn, $user_id, $wallet_id);
    if (!$coin) return ['ok' => true, 'ready' => false, 'error' => 'invalid_wallet'];

    $profile = get_user_profile_limits($conn, $user_id);
    $buy_limit = $profile['buy_limit'];

    $pricing = get_coin_buy_pricing($conn, $coin);
    $buy_rate  = $pricing['buy_rate'];
    $usd_price = $pricing['usd_price'];
    $min_swap  = (float)$pricing['min_swap'];

    $reasons = [];
    if ($min_swap > 0 && $amount_usd < $min_swap) $reasons[] = 'min_swap';
    if ($buy_limit !== null && $amount_usd > $buy_limit) $reasons[] = 'buy_limit';
    if (!$buy_rate || !$usd_price) $reasons[] = 'no_quote';

    $ready = empty($reasons);

    $ngn_cost = null;
    $coin_qty = null;
    $gateway_fee = null;
    $total_payable = null;

    if ($buy_rate && $usd_price) {
        $ngn_cost = round($amount_usd * $buy_rate, 2);
        $coin_qty = $amount_usd / $usd_price;

        $gross = gross_up_for_fee($ngn_cost);
        $fee = round(($gross - $ngn_cost), 2);

        $gateway_fee = max(0, $fee);
        $total_payable = $gross;
    }

    return [
        'ok' => true,
        'ready' => $ready,
        'coin' => $coin,
        'limits' => [
            'min_swap_usd' => $min_swap,
            'buy_limit_usd' => $buy_limit,
            'instant_buy_limit_usd' => $profile['instant_buy_limit'],
        ],
        'pricing' => [
            'buy_rate_ngn_per_usd' => $buy_rate,
            'usd_price_per_coin'   => $usd_price,
        ],
        'preview' => [
            'ngn_cost' => $ngn_cost,
            'gateway_fee_ngn' => $gateway_fee,
            'total_payable_ngn' => $total_payable,
            'coin_qty' => $coin_qty,
        ],
        'reasons' => $reasons,
        'email' => $profile['email'],
    ];
}

function insert_initiated_transaction(mysqli $conn, array $t): bool {
    $sql = "
        INSERT INTO transactions
            (coin, user_id, wallet_id, sender_address, receiver_address, amount, type, reference, provider, provider_meta, confirmation, status, applied, note)
        VALUES
            (?, ?, ?, '', ?, ?, 'buy', ?, 'paystack', ?, 0, 'initiated', 0, ?)
    ";
    $stmt = $conn->prepare($sql);
    if (!$stmt) return false;

    $stmt->bind_param(
        "sissssss",
        $t['coin'],
        $t['user_id'],
        $t['wallet_id'],
        $t['receiver_address'],
        $t['coin_qty_str'],
        $t['reference'],
        $t['provider_meta_json'],
        $t['note']
    );

    $ok = $stmt->execute();
    $stmt->close();
    return $ok;
}

function get_tx_by_reference(mysqli $conn, int $user_id, string $reference): ?array {
    $sql = "
      SELECT trans_id, coin, user_id, wallet_id, receiver_address, amount, status, applied, provider_meta
      FROM transactions
      WHERE provider='paystack' AND reference=? AND user_id=?
      LIMIT 1
    ";
    $stmt = $conn->prepare($sql);
    if (!$stmt) return null;

    $stmt->bind_param("si", $reference, $user_id);
    $stmt->execute();
    $res = $stmt->get_result();
    $row = $res ? $res->fetch_assoc() : null;
    $stmt->close();
    return $row ?: null;
}

function update_tx_status(mysqli $conn, int $trans_id, string $status, int $applied, int $confirmation, string $provider_meta_json, string $note = ''): bool {
    $sql = "
      UPDATE transactions
      SET status=?, applied=?, confirmation=?, provider_meta=?, note=?
      WHERE trans_id=?
      LIMIT 1
    ";
    $stmt = $conn->prepare($sql);
    if (!$stmt) return false;

    $stmt->bind_param("siissi", $status, $applied, $confirmation, $provider_meta_json, $note, $trans_id);
    $ok = $stmt->execute();
    $stmt->close();
    return $ok;
}

function credit_wallet(mysqli $conn, int $user_id, string $wallet_id, string $coin_qty_str): bool {
    $sql = "
      UPDATE user_wallets
      SET balance = balance + ?
      WHERE user_id=? AND wallet_id=?
      LIMIT 1
    ";
    $stmt = $conn->prepare($sql);
    if (!$stmt) return false;

    $stmt->bind_param("sis", $coin_qty_str, $user_id, $wallet_id);
    $ok = $stmt->execute();
    $stmt->close();
    return $ok;
}

/* ---------------- ACTION ROUTER ---------------- */

$action = $_GET['action'] ?? $_POST['action'] ?? null;

/* 1) QUOTE (JSON) */
if ($action === 'quote') {
    $wallet_id  = trim($_GET['wallet_id'] ?? '');
    $amount_usd = (float)($_GET['amount_usd'] ?? 0);
    $q = compute_quote($conn, $user_id, $wallet_id, $amount_usd);
    json_out($q);
}

/* 2) INIT PAYSTACK (JSON) */
if ($action === 'init_paystack') {
    global $paystackSecret;

    $wallet_id  = trim($_POST['wallet_id'] ?? '');
    $amount_usd = (float)($_POST['amount_usd'] ?? 0);

    $q = compute_quote($conn, $user_id, $wallet_id, $amount_usd);

    if (empty($q['ready'])) {
        $msg = 'Invalid request';
        if (!empty($q['reasons']) && in_array('buy_limit', $q['reasons'], true)) {
            $msg = 'Buy limit exceeded for Customer Level.';
        }
        json_out(['ok' => false, 'msg' => $msg, 'data' => $q], 400);
    }

    $email = $q['email'] ?? null;
    if (!$email) $email = 'user@example.com';

    $ngn_cost_net    = (float)($q['preview']['ngn_cost'] ?? 0);
    $gateway_fee_ngn = (float)($q['preview']['gateway_fee_ngn'] ?? 0);
    $total_payable   = (float)($q['preview']['total_payable_ngn'] ?? 0);
    $coin_qty        = (float)($q['preview']['coin_qty'] ?? 0);

    if (!($ngn_cost_net > 0) || !($total_payable > 0) || !($coin_qty > 0)) {
        json_out(['ok' => false, 'msg' => 'Unable to initialize payment'], 400);
    }

    // Amount in kobo
    $amount_kobo = (int) round($total_payable * 100);

    // Reference
    $reference = 'BCBUY-' . $user_id . '-' . time() . '-' . mt_rand(10000, 99999);

    // Store receiver address in transactions.receiver_address (internal only)
    $receiver_address = get_wallet_address_for_user($conn, $user_id, $wallet_id) ?? '';

    // Store full details internally (NOT sent to Paystack)
    $provider_meta = [
        'wallet_id' => $wallet_id,
        'receiver_address' => $receiver_address,
        'coin' => $q['coin'],
        'amount_usd' => $amount_usd,
        'ngn_cost_net' => $ngn_cost_net,
        'gateway_fee_ngn' => $gateway_fee_ngn,
        'total_payable_ngn' => $total_payable,
        'amount_kobo_expected' => $amount_kobo,
        'coin_qty' => $coin_qty,
        'buy_rate' => $q['pricing']['buy_rate_ngn_per_usd'] ?? null,
        'usd_price' => $q['pricing']['usd_price_per_coin'] ?? null,
        'limits' => $q['limits'] ?? [],
        'created_at' => date('c'),
    ];

    // Insert initiated tx
    $coin_qty_str = number_format($coin_qty, 10, '.', '');
    $ins = insert_initiated_transaction($conn, [
        'coin' => $q['coin'],
        'user_id' => $user_id,
        'wallet_id' => $wallet_id,
        'receiver_address' => $receiver_address,
        'coin_qty_str' => $coin_qty_str,
        'reference' => $reference,
        'provider_meta_json' => json_encode($provider_meta),
        'note' => 'Payment initiated',
    ]);

    if (!$ins) {
        json_out(['ok' => false, 'msg' => 'Unable to initialize payment'], 500);
    }

    if (empty($paystackSecret)) {
        json_out(['ok' => false, 'msg' => 'Payment is temporarily unavailable'], 500);
    }

    // ✅ Paystack gets NOTHING in metadata.
    // Frontend will pass only: key, email, amount, currency, ref.
    json_out([
        'ok' => true,
        'reference' => $reference,
        'email' => $email,
        'amount_kobo' => $amount_kobo,
        'currency' => 'NGN',
    ]);
}

/* 3) VERIFY PAYSTACK (JSON) */
if ($action === 'verify_paystack') {
    global $paystackSecret;

    $reference = trim($_GET['reference'] ?? '');
    if ($reference === '') {
        json_out(['ok' => false, 'msg' => 'Invalid reference'], 400);
    }

    $tx = get_tx_by_reference($conn, $user_id, $reference);
    if (!$tx) {
        json_out(['ok' => false, 'msg' => 'Transaction not found'], 404);
    }

    // Idempotency
    if (in_array($tx['status'], ['completed', 'pending'], true)) {
        $meta0 = json_decode($tx['provider_meta'] ?? '', true);
        if (!is_array($meta0)) $meta0 = [];

        json_out([
            'ok' => true,
            'already_processed' => true,
            'status' => $tx['status'],
            'reference' => $reference,
            'details' => [
                'coin' => $tx['coin'] ?? null,
                'wallet_id' => $tx['wallet_id'] ?? null,
                'receiver_address' => $tx['receiver_address'] ?? null,
                'amount_usd' => $meta0['amount_usd'] ?? null,
                'ngn_cost_net' => $meta0['ngn_cost_net'] ?? null,
                'gateway_fee_ngn' => $meta0['gateway_fee_ngn'] ?? null,
                'total_payable_ngn' => $meta0['total_payable_ngn'] ?? null,
                'coin_qty' => $meta0['coin_qty'] ?? null,
            ]
        ]);
    }

    if (empty($paystackSecret)) {
        json_out(['ok' => false, 'msg' => 'Payment verification unavailable'], 500);
    }

    // Verify Paystack (kept simple)
    $ch = curl_init('https://api.paystack.co/transaction/verify/' . rawurlencode($reference));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: Bearer ' . $paystackSecret,
        'Content-Type: application/json',
    ]);

    $resp = curl_exec($ch);
    $err  = curl_error($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $meta = json_decode($tx['provider_meta'] ?? '', true);
    if (!is_array($meta)) $meta = [];

    if ($resp === false) {
        $meta['paystack_verify_error'] = ['ok' => false, 'code' => 0, 'error' => $err ?: 'curl_error'];
        update_tx_status($conn, (int)$tx['trans_id'], 'failed', 0, 0, json_encode($meta), 'Paystack verify failed');
        json_out(['ok' => false, 'msg' => 'Verification failed'], 400);
    }

    $json = json_decode($resp, true);
    if (!is_array($json) || empty($json['status']) || empty($json['data']) || $code < 200 || $code >= 300) {
        $meta['paystack_verify_error'] = ['ok' => false, 'code' => $code, 'raw' => $resp];
        update_tx_status($conn, (int)$tx['trans_id'], 'failed', 0, 0, json_encode($meta), 'Paystack verify failed');
        json_out(['ok' => false, 'msg' => 'Verification failed'], 400);
    }

    $data = $json['data'];
    $ps_status   = $data['status'] ?? '';
    $ps_amount   = (int)($data['amount'] ?? 0);
    $ps_currency = $data['currency'] ?? 'NGN';

    $expected_kobo = (int)($meta['amount_kobo_expected'] ?? 0);

    if ($ps_status !== 'success' || $ps_currency !== 'NGN' || $expected_kobo <= 0 || $ps_amount !== $expected_kobo) {
        $meta['paystack_verify'] = $json;
        update_tx_status($conn, (int)$tx['trans_id'], 'failed', 0, 0, json_encode($meta), 'Payment not successful');
        json_out(['ok' => false, 'msg' => 'Payment not successful'], 400);
    }

    // Re-check buy limit after payment
    $amount_usd = (float)($meta['amount_usd'] ?? 0);
    $profile_now = get_user_profile_limits($conn, $user_id);
    $buy_limit_now = $profile_now['buy_limit'];

    $meta['paystack_verify'] = $json;
    $meta['verified_at'] = date('c');
    $meta['buy_limit_recheck'] = [
        'buy_limit_usd' => $buy_limit_now,
        'amount_usd' => $amount_usd,
        'checked_at' => date('c'),
    ];

    if ($buy_limit_now !== null && $amount_usd > (float)$buy_limit_now) {
        update_tx_status($conn, (int)$tx['trans_id'], 'pending', 0, 1, json_encode($meta), 'Paid but buy limit exceeded; pending review.');
        json_out([
            'ok' => true,
            'status' => 'pending',
            'reference' => $reference,
            'msg' => 'Payment received, but buy limit exceeded for Customer Level. Purchase is pending review.',
            'details' => [
                'coin' => $tx['coin'] ?? null,
                'wallet_id' => $tx['wallet_id'] ?? null,
                'receiver_address' => $tx['receiver_address'] ?? null,
                'amount_usd' => $meta['amount_usd'] ?? null,
                'ngn_cost_net' => $meta['ngn_cost_net'] ?? null,
                'gateway_fee_ngn' => $meta['gateway_fee_ngn'] ?? null,
                'total_payable_ngn' => $meta['total_payable_ngn'] ?? null,
                'coin_qty' => $meta['coin_qty'] ?? null,
            ]
        ]);
    }

    // Instant vs pending credit
    $instant_limit = $profile_now['instant_buy_limit'];
    $is_instant = false;
    if ($instant_limit !== null && $amount_usd > 0) {
        $is_instant = ($amount_usd <= (float)$instant_limit);
    }

    if (!$is_instant) {
        update_tx_status($conn, (int)$tx['trans_id'], 'pending', 0, 1, json_encode($meta), 'Paid. Awaiting processing.');
        json_out([
            'ok' => true,
            'status' => 'pending',
            'reference' => $reference,
            'msg' => 'Payment received. Your purchase is pending confirmation.',
            'details' => [
                'coin' => $tx['coin'] ?? null,
                'wallet_id' => $tx['wallet_id'] ?? null,
                'receiver_address' => $tx['receiver_address'] ?? null,
                'amount_usd' => $meta['amount_usd'] ?? null,
                'ngn_cost_net' => $meta['ngn_cost_net'] ?? null,
                'gateway_fee_ngn' => $meta['gateway_fee_ngn'] ?? null,
                'total_payable_ngn' => $meta['total_payable_ngn'] ?? null,
                'coin_qty' => $meta['coin_qty'] ?? null,
            ]
        ]);
    }

    // Instant credit
    $coin_qty  = (float)($meta['coin_qty'] ?? 0);
    $wallet_id = (string)($meta['wallet_id'] ?? $tx['wallet_id']);

    $coin_qty_str = number_format($coin_qty, 10, '.', '');
    $credited = credit_wallet($conn, $user_id, $wallet_id, $coin_qty_str);

    if (!$credited) {
        update_tx_status($conn, (int)$tx['trans_id'], 'pending', 0, 1, json_encode($meta), 'Paid. Wallet credit failed; pending review.');
        json_out([
            'ok' => true,
            'status' => 'pending',
            'reference' => $reference,
            'msg' => 'Payment received. Your purchase is pending confirmation.',
            'details' => [
                'coin' => $tx['coin'] ?? null,
                'wallet_id' => $tx['wallet_id'] ?? null,
                'receiver_address' => $tx['receiver_address'] ?? null,
                'amount_usd' => $meta['amount_usd'] ?? null,
                'ngn_cost_net' => $meta['ngn_cost_net'] ?? null,
                'gateway_fee_ngn' => $meta['gateway_fee_ngn'] ?? null,
                'total_payable_ngn' => $meta['total_payable_ngn'] ?? null,
                'coin_qty' => $meta['coin_qty'] ?? null,
            ]
        ]);
    }

    update_tx_status($conn, (int)$tx['trans_id'], 'completed', 1, 1, json_encode($meta), 'Paid and credited.');
    json_out([
        'ok' => true,
        'status' => 'completed',
        'reference' => $reference,
        'msg' => 'Payment successful. Wallet credited.',
        'details' => [
            'coin' => $tx['coin'] ?? null,
            'wallet_id' => $tx['wallet_id'] ?? null,
            'receiver_address' => $tx['receiver_address'] ?? null,
            'amount_usd' => $meta['amount_usd'] ?? null,
            'ngn_cost_net' => $meta['ngn_cost_net'] ?? null,
            'gateway_fee_ngn' => $meta['gateway_fee_ngn'] ?? null,
            'total_payable_ngn' => $meta['total_payable_ngn'] ?? null,
            'coin_qty' => $meta['coin_qty'] ?? null,
        ]
    ]);
}

/* ---------------- PAGE DATA (for include in buy.php) ---------------- */

$wallets = [];
$stmt = $conn->prepare("
    SELECT wallet_id, coin, label, icon
    FROM user_wallets
    WHERE user_id=? AND UPPER(coin) <> 'NGN'
    ORDER BY coin ASC
");
$stmt->bind_param("i", $user_id);
$stmt->execute();
$res = $stmt->get_result();
while ($row = $res->fetch_assoc()) {
    $row['coin'] = strtoupper($row['coin'] ?? '');
    $wallets[] = $row;
}
$stmt->close();

$profile = get_user_profile_limits($conn, $user_id);
$buy_limit_usd = $profile['buy_limit'];

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


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