class_database = $class_database; $this->config = require __DIR__ . '/config.rainforest.php'; $env = $this->config['rainforest']['environment']; $this->api_base_url = $this->config['rainforest']['api_base_url'][$env]; $this->headers = [ 'Content-Type: application/json', 'Authorization: Bearer ' . $this->config['rainforest']['api_key'], 'X-Merchant-ID: ' . $this->config['rainforest']['merchant_id'] ]; } /** * Create a donation payment */ public function createDonation($streamer_id, $amount, $donor_name = '', $message = '', $payment_method = 'card') { try { // Validate amount if ($amount < $this->config['rainforest']['min_donation'] || $amount > $this->config['rainforest']['max_donation']) { return [ 'success' => false, 'message' => 'Invalid donation amount. Must be between $' . $this->config['rainforest']['min_donation'] . ' and $' . $this->config['rainforest']['max_donation'] ]; } // Get streamer information $streamer = $this->getStreamerInfo($streamer_id); if (!$streamer) { return ['success' => false, 'message' => 'Streamer not found']; } // Calculate fees $platform_fee = $this->calculatePlatformFee($amount); $streamer_amount = $amount - $platform_fee; // Create payment request $payment_data = [ 'amount' => $amount * 100, // Convert to cents 'currency' => $this->config['rainforest']['currency'], 'payment_method' => $payment_method, 'description' => "Donation to {$streamer['username']}", 'metadata' => [ 'type' => 'donation', 'streamer_id' => $streamer_id, 'streamer_username' => $streamer['username'], 'donor_name' => $donor_name, 'message' => $message, 'platform_fee' => $platform_fee, 'streamer_amount' => $streamer_amount ], 'webhook_url' => $this->config['rainforest']['webhook_url'], 'return_url' => "https://{$_SERVER['HTTP_HOST']}/donations/success", 'cancel_url' => "https://{$_SERVER['HTTP_HOST']}/donations/cancel" ]; // Make API request $response = $this->makeApiRequest('POST', '/payments', $payment_data); if ($response && isset($response['id'])) { // Store pending donation in database $donation_id = $this->storePendingDonation([ 'streamer_id' => $streamer_id, 'amount' => $amount, 'platform_fee' => $platform_fee, 'streamer_amount' => $streamer_amount, 'donor_name' => $donor_name, 'message' => $message, 'payment_method' => $payment_method, 'rainforest_payment_id' => $response['id'], 'status' => 'pending' ]); return [ 'success' => true, 'payment_id' => $response['id'], 'donation_id' => $donation_id, 'payment_url' => $response['payment_url'] ?? null, 'message' => 'Payment created successfully' ]; } else { return [ 'success' => false, 'message' => 'Failed to create payment: ' . ($response['error'] ?? 'Unknown error') ]; } } catch (Exception $e) { $this->logError('Create Donation Error', $e->getMessage()); return [ 'success' => false, 'message' => 'Error processing donation: ' . $e->getMessage() ]; } } /** * Create a payment (supports both donations and token purchases) */ public function createPayment($payment_data) { try { // Make API request $response = $this->makeApiRequest('POST', '/payments', $payment_data); if ($response && isset($response['id'])) { return [ 'success' => true, 'payment_id' => $response['id'], 'payment_url' => $response['payment_url'] ?? null, 'message' => 'Payment created successfully' ]; } else { return [ 'success' => false, 'message' => 'Failed to create payment: ' . ($response['error'] ?? 'Unknown error') ]; } } catch (Exception $e) { $this->logError('Create Payment Error', $e->getMessage()); return [ 'success' => false, 'message' => 'Error creating payment: ' . $e->getMessage() ]; } } /** * Create a payout (for token redemptions) */ public function createPayout($payout_data) { try { // Make API request $response = $this->makeApiRequest('POST', '/payouts', $payout_data); if ($response && isset($response['id'])) { return [ 'success' => true, 'payout_id' => $response['id'], 'message' => 'Payout created successfully' ]; } else { return [ 'success' => false, 'message' => 'Failed to create payout: ' . ($response['error'] ?? 'Unknown error') ]; } } catch (Exception $e) { $this->logError('Create Payout Error', $e->getMessage()); return [ 'success' => false, 'message' => 'Error creating payout: ' . $e->getMessage() ]; } } /** * Process webhook from Rainforest Pay */ public function processWebhook($payload, $signature) { try { // Verify webhook signature if (!$this->verifyWebhookSignature($payload, $signature)) { return ['success' => false, 'message' => 'Invalid webhook signature']; } $data = json_decode($payload, true); if (!$data) { return ['success' => false, 'message' => 'Invalid webhook payload']; } $event_type = $data['event'] ?? ''; $payment_data = $data['data'] ?? []; switch ($event_type) { case 'payment.completed': return $this->handlePaymentCompleted($payment_data); case 'payment.failed': return $this->handlePaymentFailed($payment_data); case 'payment.cancelled': return $this->handlePaymentCancelled($payment_data); case 'payout.completed': return $this->handlePayoutCompleted($payment_data); case 'payout.failed': return $this->handlePayoutFailed($payment_data); default: $this->logError('Unknown Webhook Event', $event_type); return ['success' => false, 'message' => 'Unknown event type']; } } catch (Exception $e) { $this->logError('Webhook Processing Error', $e->getMessage()); return ['success' => false, 'message' => 'Webhook processing failed']; } } /** * Handle completed payment */ private function handlePaymentCompleted($payment_data) { $payment_id = $payment_data['id']; $metadata = $payment_data['metadata'] ?? []; $payment_type = $metadata['type'] ?? 'donation'; try { if ($payment_type === 'token_purchase') { return $this->handleTokenPurchaseCompleted($payment_data, $metadata); } else { // Handle regular donation $sql = "UPDATE donations SET status = 'completed', completed_at = NOW(), rainforest_data = ? WHERE rainforest_payment_id = ?"; $this->class_database->execute($sql, [ json_encode($payment_data), $payment_id ]); // Update streamer balance if (isset($metadata['streamer_id']) && isset($metadata['streamer_amount'])) { $this->updateStreamerBalance($metadata['streamer_id'], $metadata['streamer_amount']); } // Send notification to streamer $this->sendDonationNotification($metadata); return ['success' => true, 'message' => 'Payment completed successfully']; } } catch (Exception $e) { $this->logError('Payment Completion Error', $e->getMessage()); return ['success' => false, 'message' => 'Error updating payment status']; } } /** * Handle completed token purchase */ private function handleTokenPurchaseCompleted($payment_data, $metadata) { $payment_id = $payment_data['id']; $purchase_id = $metadata['purchase_id'] ?? null; $user_id = $metadata['user_id'] ?? null; $token_amount = $metadata['token_amount'] ?? 0; if (!$purchase_id || !$user_id || !$token_amount) { return ['success' => false, 'message' => 'Invalid token purchase metadata']; } try { $this->class_database->beginTransaction(); // Update purchase record $sql = "UPDATE token_purchases SET status = 'completed', updated_at = NOW() WHERE id = ? AND payment_id = ?"; $this->class_database->execute($sql, [$purchase_id, $payment_id]); // Add tokens to user balance require_once '../../../f_core/f_classes/class.token.php'; VToken::recordTransaction($user_id, $token_amount, 'purchase', "Token purchase #{$purchase_id}"); $this->class_database->commitTransaction(); $this->logInfo('Token Purchase Completed', "Purchase ID: {$purchase_id}, User: {$user_id}, Tokens: {$token_amount}"); return ['success' => true, 'message' => 'Token purchase completed successfully']; } catch (Exception $e) { $this->class_database->rollbackTransaction(); $this->logError('Token Purchase Completion Error', $e->getMessage()); return ['success' => false, 'message' => 'Error completing token purchase']; } } /** * Handle failed payment */ private function handlePaymentFailed($payment_data) { $payment_id = $payment_data['id']; $sql = "UPDATE donations SET status = 'failed', failed_at = NOW(), failure_reason = ?, rainforest_data = ? WHERE rainforest_payment_id = ?"; $this->class_database->execute($sql, [ $payment_data['failure_reason'] ?? 'Payment failed', json_encode($payment_data), $payment_id ]); return ['success' => true, 'message' => 'Payment failure recorded']; } /** * Handle completed payout */ private function handlePayoutCompleted($payout_data) { $payout_id = $payout_data['id']; $metadata = $payout_data['metadata'] ?? []; $payout_type = $metadata['type'] ?? 'streamer_payout'; try { if ($payout_type === 'token_redemption') { return $this->handleTokenRedemptionCompleted($payout_data, $metadata); } else { // Handle regular streamer payout $sql = "UPDATE payouts SET status = 'completed', completed_at = NOW(), rainforest_data = ? WHERE rainforest_payout_id = ?"; $this->class_database->execute($sql, [ json_encode($payout_data), $payout_id ]); return ['success' => true, 'message' => 'Payout completed successfully']; } } catch (Exception $e) { $this->logError('Payout Completion Error', $e->getMessage()); return ['success' => false, 'message' => 'Error updating payout status']; } } /** * Handle failed payout */ private function handlePayoutFailed($payout_data) { $payout_id = $payout_data['id']; $metadata = $payout_data['metadata'] ?? []; $payout_type = $metadata['type'] ?? 'streamer_payout'; try { if ($payout_type === 'token_redemption') { return $this->handleTokenRedemptionFailed($payout_data, $metadata); } else { // Handle regular streamer payout failure $sql = "UPDATE payouts SET status = 'failed', failed_at = NOW(), failure_reason = ?, rainforest_data = ? WHERE rainforest_payout_id = ?"; $this->class_database->execute($sql, [ $payout_data['failure_reason'] ?? 'Payout failed', json_encode($payout_data), $payout_id ]); return ['success' => true, 'message' => 'Payout failure recorded']; } } catch (Exception $e) { $this->logError('Payout Failure Error', $e->getMessage()); return ['success' => false, 'message' => 'Error updating payout status']; } } /** * Handle completed token redemption */ private function handleTokenRedemptionCompleted($payout_data, $metadata) { $payout_id = $payout_data['id']; $redemption_id = $metadata['redemption_id'] ?? null; $user_id = $metadata['user_id'] ?? null; if (!$redemption_id || !$user_id) { return ['success' => false, 'message' => 'Invalid token redemption metadata']; } try { $this->class_database->beginTransaction(); // Update payout record $sql = "UPDATE token_payouts SET status = 'completed', completed_at = NOW() WHERE rainforest_payout_id = ?"; $this->class_database->execute($sql, [$payout_id]); // Update redemption record $sql = "UPDATE token_redemptions SET status = 'completed', completed_at = NOW() WHERE id = ?"; $this->class_database->execute($sql, [$redemption_id]); $this->class_database->commitTransaction(); $this->logInfo('Token Redemption Completed', "Redemption ID: {$redemption_id}, User: {$user_id}"); return ['success' => true, 'message' => 'Token redemption completed successfully']; } catch (Exception $e) { $this->class_database->rollbackTransaction(); $this->logError('Token Redemption Completion Error', $e->getMessage()); return ['success' => false, 'message' => 'Error completing token redemption']; } } /** * Handle failed token redemption */ private function handleTokenRedemptionFailed($payout_data, $metadata) { $payout_id = $payout_data['id']; $redemption_id = $metadata['redemption_id'] ?? null; $user_id = $metadata['user_id'] ?? null; $token_amount = $metadata['token_amount'] ?? 0; if (!$redemption_id || !$user_id || !$token_amount) { return ['success' => false, 'message' => 'Invalid token redemption metadata']; } try { $this->class_database->beginTransaction(); // Update payout record $sql = "UPDATE token_payouts SET status = 'failed', failed_at = NOW(), failure_reason = ? WHERE rainforest_payout_id = ?"; $this->class_database->execute($sql, [ $payout_data['failure_reason'] ?? 'Payout failed', $payout_id ]); // Update redemption record $sql = "UPDATE token_redemptions SET status = 'failed', failed_at = NOW() WHERE id = ?"; $this->class_database->execute($sql, [$redemption_id]); // Refund tokens to user require_once '../../../f_core/f_classes/class.token.php'; VToken::recordTransaction($user_id, $token_amount, 'refund', "Redemption refund #{$redemption_id}"); $this->class_database->commitTransaction(); $this->logInfo('Token Redemption Failed - Refunded', "Redemption ID: {$redemption_id}, User: {$user_id}, Tokens: {$token_amount}"); return ['success' => true, 'message' => 'Token redemption failed - tokens refunded']; } catch (Exception $e) { $this->class_database->rollbackTransaction(); $this->logError('Token Redemption Failure Error', $e->getMessage()); return ['success' => false, 'message' => 'Error handling token redemption failure']; } } /** * Request payout for streamer */ public function requestPayout($streamer_id) { try { $balance = $this->getStreamerBalance($streamer_id); if ($balance < $this->config['rainforest']['min_payout']) { return [ 'success' => false, 'message' => 'Insufficient balance for payout. Minimum: $' . $this->config['rainforest']['min_payout'] ]; } // Get streamer payout details $streamer = $this->getStreamerPayoutInfo($streamer_id); if (!$streamer || !$streamer['payout_method']) { return [ 'success' => false, 'message' => 'Payout method not configured. Please update your payout settings.' ]; } // Calculate payout amount after fees $fee_amount = $this->calculatePayoutFee($balance); $payout_amount = $balance - $fee_amount; // Create payout request $payout_data = [ 'amount' => $payout_amount * 100, // Convert to cents 'currency' => $this->config['rainforest']['currency'], 'recipient' => [ 'type' => $streamer['payout_method'], 'details' => json_decode($streamer['payout_details'], true) ], 'description' => "Payout to {$streamer['username']}", 'metadata' => [ 'type' => 'streamer_payout', 'streamer_id' => $streamer_id, 'original_balance' => $balance, 'fee_amount' => $fee_amount, 'payout_amount' => $payout_amount ] ]; $response = $this->makeApiRequest('POST', '/payouts', $payout_data); if ($response && isset($response['id'])) { // Record payout request $this->recordPayoutRequest($streamer_id, $payout_amount, $fee_amount, $response['id']); return [ 'success' => true, 'payout_id' => $response['id'], 'amount' => $payout_amount, 'fee' => $fee_amount, 'message' => 'Payout request submitted successfully' ]; } else { return [ 'success' => false, 'message' => 'Failed to create payout: ' . ($response['error'] ?? 'Unknown error') ]; } } catch (Exception $e) { $this->logError('Payout Request Error', $e->getMessage()); return [ 'success' => false, 'message' => 'Error processing payout: ' . $e->getMessage() ]; } } /** * Get available payment methods */ public function getPaymentMethods() { $methods = []; $config_methods = $this->config['rainforest']['payment_methods']; if ($config_methods['card']) { $methods[] = [ 'id' => 'card', 'name' => 'Credit/Debit Card', 'icon' => 'credit-card', 'description' => 'Visa, Mastercard, American Express' ]; } if ($config_methods['bank_transfer']) { $methods[] = [ 'id' => 'bank_transfer', 'name' => 'Bank Transfer', 'icon' => 'bank', 'description' => 'Direct bank transfer' ]; } if ($config_methods['mobile_money']) { $methods[] = [ 'id' => 'mobile_money', 'name' => 'Mobile Money', 'icon' => 'mobile', 'description' => 'MTN, Airtel, Vodafone' ]; } if ($config_methods['wallet']) { $methods[] = [ 'id' => 'wallet', 'name' => 'Digital Wallet', 'icon' => 'wallet', 'description' => 'PayPal, Apple Pay, Google Pay' ]; } return $methods; } /** * Get donation history for streamer */ public function getDonationHistory($streamer_id, $limit = 20, $offset = 0) { $sql = "SELECT d.*, u.usr_user as donor_username FROM donations d LEFT JOIN db_accountuser u ON d.donor_id = u.usr_id WHERE d.streamer_id = ? AND d.status = 'completed' ORDER BY d.completed_at DESC LIMIT ? OFFSET ?"; return $this->class_database->getRows($sql, [$streamer_id, $limit, $offset]); } /** * Get streamer earnings summary */ public function getStreamerEarnings($streamer_id, $period = '30_days') { $date_condition = $this->getDateCondition($period); $sql = "SELECT COUNT(*) as total_donations, SUM(amount) as total_amount, SUM(streamer_amount) as total_earned, SUM(platform_fee) as total_fees, AVG(amount) as average_donation FROM donations WHERE streamer_id = ? AND status = 'completed' AND $date_condition"; return $this->class_database->getRow($sql, [$streamer_id]); } // Helper Methods private function makeApiRequest($method, $endpoint, $data = null) { $url = $this->api_base_url . $endpoint; $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $this->headers, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => true ]); if ($method === 'POST' && $data) { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($response === false) { throw new Exception('API request failed'); } $decoded = json_decode($response, true); if ($http_code >= 400) { $this->logError('API Error', "HTTP $http_code: " . ($decoded['message'] ?? $response)); return ['error' => $decoded['message'] ?? 'API request failed']; } return $decoded; } private function verifyWebhookSignature($payload, $signature) { $expected = hash_hmac('sha256', $payload, $this->config['rainforest']['webhook_secret']); return hash_equals($expected, $signature); } private function calculatePlatformFee($amount) { $percentage_fee = $amount * ($this->config['rainforest']['platform_fee_percentage'] / 100); $fixed_fee = $this->config['rainforest']['platform_fee_fixed']; return $percentage_fee + $fixed_fee; } private function calculatePayoutFee($amount) { $percentage_fee = $amount * ($this->config['rainforest']['payout_fee_percentage'] / 100); $fixed_fee = $this->config['rainforest']['payout_fee_fixed']; return $percentage_fee + $fixed_fee; } private function getStreamerInfo($streamer_id) { $sql = "SELECT usr_id, usr_user as username, usr_dname as display_name FROM db_accountuser WHERE usr_id = ?"; return $this->class_database->getRow($sql, [$streamer_id]); } private function getStreamerBalance($streamer_id) { $sql = "SELECT COALESCE(SUM(streamer_amount), 0) as balance FROM donations WHERE streamer_id = ? AND status = 'completed' AND payout_id IS NULL"; $result = $this->class_database->getRow($sql, [$streamer_id]); return $result['balance'] ?? 0; } private function updateStreamerBalance($streamer_id, $amount) { // Balance is calculated dynamically, but we can update a cached value if needed $sql = "UPDATE db_accountuser SET donation_balance = COALESCE(donation_balance, 0) + ? WHERE usr_id = ?"; $this->class_database->execute($sql, [$amount, $streamer_id]); } private function storePendingDonation($data) { $sql = "INSERT INTO donations ( streamer_id, amount, platform_fee, streamer_amount, donor_name, message, payment_method, rainforest_payment_id, status, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())"; $this->class_database->execute($sql, [ $data['streamer_id'], $data['amount'], $data['platform_fee'], $data['streamer_amount'], $data['donor_name'], $data['message'], $data['payment_method'], $data['rainforest_payment_id'], $data['status'] ]); return $this->class_database->lastInsertId(); } private function recordPayoutRequest($streamer_id, $amount, $fee, $payout_id) { $sql = "INSERT INTO payouts ( streamer_id, amount, fee, rainforest_payout_id, status, created_at ) VALUES (?, ?, ?, ?, 'pending', NOW())"; $this->class_database->execute($sql, [$streamer_id, $amount, $fee, $payout_id]); // Mark donations as paid out $sql = "UPDATE donations SET payout_id = ? WHERE streamer_id = ? AND status = 'completed' AND payout_id IS NULL"; $this->class_database->execute($sql, [$payout_id, $streamer_id]); } private function logError($type, $message) { error_log(date('Y-m-d H:i:s') . " [RainforestPay] ERROR - $type: $message\n", 3, 'logs/rainforest_pay.log'); } private function logInfo($type, $message) { error_log(date('Y-m-d H:i:s') . " [RainforestPay] INFO - $type: $message\n", 3, 'logs/rainforest_pay.log'); } private function sendDonationNotification($metadata) { // Integration with EasyStream notification system // This would trigger notifications to the streamer about new donations } private function getDateCondition($period) { switch ($period) { case '7_days': return "completed_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)"; case '30_days': return "completed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)"; case '90_days': return "completed_at >= DATE_SUB(NOW(), INTERVAL 90 DAY)"; case '1_year': return "completed_at >= DATE_SUB(NOW(), INTERVAL 1 YEAR)"; default: return "completed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)"; } } } // Initialize handler if accessed directly if (basename(__FILE__) == basename($_SERVER['SCRIPT_NAME'])) { $handler = new RainforestPayHandler($class_database); // Handle different actions $action = $_GET['action'] ?? $_POST['action'] ?? ''; switch ($action) { case 'create_donation': $result = $handler->createDonation( $_POST['streamer_id'], floatval($_POST['amount']), $_POST['donor_name'] ?? '', $_POST['message'] ?? '', $_POST['payment_method'] ?? 'card' ); header('Content-Type: application/json'); echo json_encode($result); break; case 'webhook': $payload = file_get_contents('php://input'); $signature = $_SERVER['HTTP_X_RAINFOREST_SIGNATURE'] ?? ''; $result = $handler->processWebhook($payload, $signature); header('Content-Type: application/json'); echo json_encode($result); break; case 'request_payout': $result = $handler->requestPayout($_POST['streamer_id']); header('Content-Type: application/json'); echo json_encode($result); break; case 'get_payment_methods': $methods = $handler->getPaymentMethods(); header('Content-Type: application/json'); echo json_encode(['success' => true, 'methods' => $methods]); break; case 'create_payment': $result = $handler->createPayment($_POST); header('Content-Type: application/json'); echo json_encode($result); break; case 'create_payout': $result = $handler->createPayout($_POST); header('Content-Type: application/json'); echo json_encode($result); break; default: header('Content-Type: application/json'); echo json_encode(['success' => false, 'message' => 'Invalid action']); } } ?>