Files
easystream/modules/m_frontend/m_donations/rainforest_pay.php
T
Krystie 092a8bc7ce refactor: organize codebase and remove redundant files
- Removed all backup/duplicate files
- Removed test files from root
- Consolidated documentation to /docs/
- Moved scripts to /scripts/
- Renamed f_* directories (removed prefix)
- Organized icons and assets
- Removed unused vendor directories
- Cleaned up redundant config files
2026-03-30 16:34:45 -07:00

813 lines
32 KiB
PHP

<?php
/*******************************************************************************************************************
| Rainforest Pay Integration for EasyStream
| Complete payment processing with Rainforest Pay API
|*******************************************************************************************************************/
define('_ISVALID', true);
include_once '../../../f_core/config.core.php';
class RainforestPayHandler {
private $config;
private $class_database;
private $api_base_url;
private $headers;
public function __construct($class_database) {
$this->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']);
}
}
?>