T.ME/BIBIL_0DAY
CasperSecurity


Server : Apache/2
System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64
User : gositeme ( 1004)
PHP Version : 8.2.29
Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname
Directory :  /home/gositeme/domains/soundstudiopro.com/public_html/webhooks/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/gositeme/domains/soundstudiopro.com/public_html/webhooks/stripe.php
<?php
// Stripe Webhook Handler
// Handles incoming webhook events from Stripe

// Set content type to JSON
header('Content-Type: application/json');

// SECURITY: Check if Stripe library is available - REQUIRED for verification
if (!class_exists('Stripe\Stripe')) {
    // CRITICAL: Do NOT accept webhooks without verification
    error_log("SECURITY: Stripe library not available - rejecting webhook without verification");
    http_response_code(503);
    echo json_encode(['error' => 'Service unavailable - Stripe library required']);
    exit();
}

// SECURITY: Load Stripe credentials from config file or environment variables
// Priority: 1. Environment variables, 2. Config file, 3. Fallback (for backward compatibility)
$webhook_secret = $_ENV['STRIPE_WEBHOOK_SECRET'] ?? getenv('STRIPE_WEBHOOK_SECRET') ?: null;
$stripe_secret_key = $_ENV['STRIPE_SECRET_KEY'] ?? getenv('STRIPE_SECRET_KEY') ?: null;

// Try to load from config file if not in environment
if (!$webhook_secret || !$stripe_secret_key) {
    $stripe_config_file = __DIR__ . '/../config/stripe.env.php';
    if (file_exists($stripe_config_file)) {
        require_once $stripe_config_file;
        $webhook_secret = defined('STRIPE_WEBHOOK_SECRET') ? STRIPE_WEBHOOK_SECRET : $webhook_secret;
        $stripe_secret_key = defined('STRIPE_SECRET_KEY') ? STRIPE_SECRET_KEY : $stripe_secret_key;
    }
}

// SECURITY: Fail if secrets are not configured (no unsafe fallbacks)
if (!$webhook_secret) {
    error_log("CRITICAL: STRIPE_WEBHOOK_SECRET not configured. Check environment variables or config/stripe.env.php");
    http_response_code(500);
    echo json_encode(['error' => 'Webhook configuration error']);
    exit();
}
if (!$stripe_secret_key) {
    error_log("CRITICAL: STRIPE_SECRET_KEY not configured. Check environment variables or config/stripe.env.php");
    http_response_code(500);
    echo json_encode(['error' => 'Webhook configuration error']);
    exit();
}

// SECURITY: Optional IP whitelist check (defense in depth)
// Stripe webhook IPs: https://stripe.com/docs/ips
$stripe_ips = [
    '3.18.12.63', '3.130.192.231', '13.235.14.237', '13.235.122.149', '18.211.135.69',
    '35.154.171.200', '52.15.183.38', '54.187.174.169', '54.187.205.235', '54.187.216.72',
    '54.241.31.99', '54.241.31.102', '54.241.34.107', '104.45.136.0/24', '104.45.137.0/24'
];
$client_ip = $_SERVER['REMOTE_ADDR'] ?? '';
$is_stripe_ip = false;
foreach ($stripe_ips as $stripe_ip) {
    if (strpos($stripe_ip, '/') !== false) {
        // CIDR notation - simple check (for production, use proper CIDR matching)
        $ip_parts = explode('/', $stripe_ip);
        $network = ip2long($ip_parts[0]);
        $mask = ~((1 << (32 - $ip_parts[1])) - 1);
        $client_long = ip2long($client_ip);
        if (($client_long & $mask) === ($network & $mask)) {
            $is_stripe_ip = true;
            break;
        }
    } else {
        if ($client_ip === $stripe_ip) {
            $is_stripe_ip = true;
            break;
        }
    }
}

// Log IP check (but don't block - signature verification is primary security)
if (!$is_stripe_ip && !empty($client_ip)) {
    error_log("WARNING: Webhook request from non-Stripe IP: {$client_ip} (signature verification will be performed)");
}

// Get the raw POST data
$payload = file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';

// SECURITY: Require signature header
if (empty($sig_header)) {
    error_log("SECURITY: Webhook request missing signature header from IP: {$client_ip}");
    http_response_code(400);
    echo json_encode(['error' => 'Missing signature header']);
    exit();
}

// Verify webhook signature
try {
    $event = \Stripe\Webhook::constructEvent(
        $payload, $sig_header, $webhook_secret
    );
} catch(\UnexpectedValueException $e) {
    // Invalid payload
    http_response_code(400);
    echo json_encode(['error' => 'Invalid payload']);
    exit();
} catch(\Stripe\Exception\SignatureVerificationException $e) {
    // Invalid signature
    http_response_code(400);
    echo json_encode(['error' => 'Invalid signature']);
    exit();
}

// Log the webhook event
$log_data = [
    'timestamp' => date('Y-m-d H:i:s'),
    'event_type' => $event->type,
    'event_id' => $event->id,
    'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
    'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
];

// Write to log file
$log_file = __DIR__ . '/../logs/stripe_webhooks.log';
$log_entry = json_encode($log_data) . "\n";
file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX);

// Handle the event
switch ($event->type) {
    case 'payment_intent.succeeded':
        $paymentIntent = $event->data->object;
        
        // Log successful payment
        $success_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'event' => 'payment_intent.succeeded',
            'payment_intent_id' => $paymentIntent->id,
            'amount' => $paymentIntent->amount / 100, // Convert from cents
            'currency' => $paymentIntent->currency,
            'customer_id' => $paymentIntent->customer ?? 'unknown',
            'metadata' => $paymentIntent->metadata ?? []
        ];
        
        // Write to success log
        $success_log_file = __DIR__ . '/../logs/stripe_success.log';
        file_put_contents($success_log_file, json_encode($success_log) . "\n", FILE_APPEND | LOCK_EX);
        
        // Update your database or trigger other actions
        try {
            handleSuccessfulPayment($paymentIntent);
        } catch (Exception $e) {
            // Log webhook processing error but still return 200 to Stripe
            // (we'll retry via the retry queue)
            error_log("Webhook processing error for payment_intent.succeeded: " . $e->getMessage());
            $error_log = [
                'timestamp' => date('Y-m-d H:i:s'),
                'event' => 'payment_intent.succeeded_processing_error',
                'payment_intent_id' => $paymentIntent->id,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString()
            ];
            $error_log_file = __DIR__ . '/../logs/stripe_webhook_errors.log';
            file_put_contents($error_log_file, json_encode($error_log) . "\n", FILE_APPEND | LOCK_EX);
        }
        break;
        
    case 'payment_intent.payment_failed':
        $paymentIntent = $event->data->object;
        
        // Log failed payment
        $failure_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'event' => 'payment_intent.payment_failed',
            'payment_intent_id' => $paymentIntent->id,
            'amount' => $paymentIntent->amount / 100,
            'currency' => $paymentIntent->currency,
            'customer_id' => $paymentIntent->customer ?? 'unknown',
            'last_payment_error' => $paymentIntent->last_payment_error ?? null
        ];
        
        // Write to failure log
        $failure_log_file = __DIR__ . '/../logs/stripe_failures.log';
        file_put_contents($failure_log_file, json_encode($failure_log) . "\n", FILE_APPEND | LOCK_EX);
        
        // Handle failed payment
        handleFailedPayment($paymentIntent);
        break;
        
    case 'customer.subscription.created':
        $subscription = $event->data->object;
        try {
            handleSubscriptionCreated($subscription);
            http_response_code(200);
            echo json_encode(['status' => 'success']);
        } catch (Exception $e) {
            error_log("Webhook error (customer.subscription.created): " . $e->getMessage());
            
            // Return 500 for transient errors (Stripe will retry)
            // Return 400 for permanent errors (Stripe won't retry)
            if (strpos($e->getMessage(), 'User not found') !== false) {
                // Permanent error - don't retry
                http_response_code(400);
                echo json_encode(['error' => 'User not found']);
            } else {
                // Transient error - Stripe will retry
                http_response_code(500);
                echo json_encode(['error' => 'Processing error', 'message' => $e->getMessage()]);
            }
        }
        break;
        
    case 'customer.subscription.updated':
        $subscription = $event->data->object;
        try {
            handleSubscriptionUpdated($subscription);
            http_response_code(200);
            echo json_encode(['status' => 'success']);
        } catch (Exception $e) {
            error_log("Webhook error (customer.subscription.updated): " . $e->getMessage());
            
            // Return 500 for transient errors (Stripe will retry)
            if (strpos($e->getMessage(), 'User not found') !== false) {
                http_response_code(400);
                echo json_encode(['error' => 'User not found']);
            } else {
                http_response_code(500);
                echo json_encode(['error' => 'Processing error', 'message' => $e->getMessage()]);
            }
        }
        break;
        
    case 'customer.subscription.deleted':
        $subscription = $event->data->object;
        handleSubscriptionDeleted($subscription);
        break;
        
    case 'invoice.payment_succeeded':
        $invoice = $event->data->object;
        handleInvoicePaymentSucceeded($invoice);
        break;
        
    case 'invoice.payment_failed':
        $invoice = $event->data->object;
        handleInvoicePaymentFailed($invoice);
        break;
        
    case 'checkout.session.completed':
        // Backup handler - subscription should already be recorded in subscription_success.php
        // But this ensures we catch it if the user doesn't complete the redirect
        $session = $event->data->object;
        if ($session->mode === 'subscription' && !empty($session->subscription)) {
            // Fetch subscription and record it (same logic as subscription_success.php)
            try {
                require_once __DIR__ . '/../config/database.php';
                $pdo = getDBConnection();
                
                // Get subscription from Stripe
                $stripe_secret = getStripeSecretKey();
                $ch = curl_init();
                curl_setopt($ch, CURLOPT_URL, 'https://api.stripe.com/v1/subscriptions/' . urlencode($session->subscription));
                curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $stripe_secret]);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                $response = curl_exec($ch);
                $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                curl_close($ch);
                
                if ($http_code === 200) {
                    $subscription = json_decode($response, true);
                    // Use existing handleSubscriptionCreated function
                    $subscription_obj = json_decode(json_encode($subscription)); // Convert to object
                    handleSubscriptionCreated($subscription_obj);
                } else {
                    error_log("Failed to fetch subscription in checkout.session.completed: HTTP {$http_code}");
                }
            } catch (Exception $e) {
                error_log("Error in checkout.session.completed handler: " . $e->getMessage());
                // Don't return error - this is a backup handler, primary recording happens in subscription_success.php
            }
        }
        break;
        
    default:
        // Unexpected event type
        $unknown_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'event' => 'unknown_event_type',
            'event_type' => $event->type,
            'event_id' => $event->id
        ];
        
        $unknown_log_file = __DIR__ . '/../logs/stripe_unknown.log';
        file_put_contents($unknown_log_file, json_encode($unknown_log) . "\n", FILE_APPEND | LOCK_EX);
        break;
}

// Return success response
http_response_code(200);
echo json_encode(['status' => 'success']);

// Helper function to get Stripe secret key (loads from config/env if not already loaded)
function getStripeSecretKey() {
    static $cached_key = null;
    
    if ($cached_key !== null) {
        return $cached_key;
    }
    
    // Try environment variables first
    $key = $_ENV['STRIPE_SECRET_KEY'] ?? getenv('STRIPE_SECRET_KEY') ?: null;
    
    // Try config file if not in environment
    if (!$key) {
        $stripe_config_file = __DIR__ . '/../config/stripe.env.php';
        if (file_exists($stripe_config_file)) {
            require_once $stripe_config_file;
            $key = defined('STRIPE_SECRET_KEY') ? STRIPE_SECRET_KEY : null;
        }
    }
    
    // SECURITY: Fail if key is not configured (no unsafe fallbacks)
    if (!$key) {
        error_log("CRITICAL: STRIPE_SECRET_KEY not configured in getStripeSecretKey()");
        throw new Exception("Stripe secret key not configured");
    }
    
    $cached_key = $key;
    return $key;
}

// Helper functions
function handleSuccessfulPayment($paymentIntent) {
    // Extract metadata for credit purchases
    // Stripe metadata is an object, convert to array for easier access
    $metadata_obj = $paymentIntent->metadata ?? (object)[];
    $metadata = [];
    foreach ($metadata_obj as $key => $value) {
        $metadata[$key] = $value;
    }
    
    $user_id = $metadata['user_id'] ?? null;
    $credits = $metadata['credits'] ?? $metadata['total_credits'] ?? 0;
    $package = $metadata['package'] ?? 'unknown';
    $subscription_period = $metadata['subscription_period'] ?? '30_days';
    $purchase_type = $metadata['purchase_type'] ?? 'credit_package';
    $track_id = $metadata['track_id'] ?? null;
    $payment_type = $metadata['payment_type'] ?? 'unknown';
    $has_tracks = $metadata['has_tracks'] ?? false;
    
    // Log the action
    $action_log = [
        'timestamp' => date('Y-m-d H:i:s'),
        'action' => 'handleSuccessfulPayment',
        'payment_intent_id' => $paymentIntent->id,
        'amount' => $paymentIntent->amount / 100,
        'currency' => $paymentIntent->currency,
        'user_id' => $user_id,
        'credits' => $credits,
        'package' => $package,
        'purchase_type' => $purchase_type,
        'track_id' => $track_id,
        'payment_type' => $payment_type,
        'has_tracks' => $has_tracks
    ];
    
    $action_log_file = __DIR__ . '/../logs/stripe_actions.log';
    file_put_contents($action_log_file, json_encode($action_log) . "\n", FILE_APPEND | LOCK_EX);
    
    // Handle mixed cart checkout
    if ($payment_type === 'mixed_cart_checkout' && $user_id) {
        $result = processMixedCartPayment($paymentIntent, $metadata);
        if (!$result['success']) {
            // Log failure and schedule retry
            schedulePurchaseRetry($paymentIntent->id, 'mixed_cart', $metadata);
        }
    } else if ($purchase_type === 'event_ticket' && isset($metadata['event_id'])) {
        // Handle event ticket purchase
        try {
            processEventTicketPurchase($user_id, $metadata['event_id'], $paymentIntent);
        } catch (Exception $e) {
            error_log("Event ticket purchase failed: " . $e->getMessage());
            schedulePurchaseRetry($paymentIntent->id, 'event_ticket', $metadata);
        }
    } else if ($purchase_type === 'track_purchase' && $track_id) {
        // Handle individual track purchase with retry logic
        $result = processTrackPurchaseWithRetry($user_id, $track_id, $credits, $paymentIntent->id);
        if (!$result['success']) {
            // Schedule retry for later
            schedulePurchaseRetry($paymentIntent->id, 'track', ['user_id' => $user_id, 'track_id' => $track_id, 'credits_used' => $credits]);
        }
    } else if ($user_id && $credits > 0) {
        // Handle credit package purchase
        try {
            addCreditsToUser($user_id, $credits, $package, $subscription_period, $paymentIntent->id);
        } catch (Exception $e) {
            error_log("Credit purchase failed: " . $e->getMessage());
            schedulePurchaseRetry($paymentIntent->id, 'credits', $metadata);
        }
    }
}

/**
 * Process track purchase with automatic retry on failure
 * Retries up to 3 times with exponential backoff
 */
function processTrackPurchaseWithRetry($user_id, $track_id, $credits_used, $payment_intent_id, $max_retries = 3) {
    $attempt = 0;
    $last_error = null;
    
    while ($attempt < $max_retries) {
        $attempt++;
        $result = processTrackPurchase($user_id, $track_id, $credits_used, $payment_intent_id, true);
        
        if ($result['success']) {
            if ($attempt > 1) {
                // Log successful retry
                $retry_log = [
                    'timestamp' => date('Y-m-d H:i:s'),
                    'action' => 'track_purchase_retry_success',
                    'user_id' => $user_id,
                    'track_id' => $track_id,
                    'payment_intent_id' => $payment_intent_id,
                    'attempt' => $attempt
                ];
                $log_file = __DIR__ . '/../logs/track_purchase_retries.log';
                file_put_contents($log_file, json_encode($retry_log) . "\n", FILE_APPEND | LOCK_EX);
            }
            return $result;
        }
        
        $last_error = $result['message'];
        
        // If it's a non-retryable error (track not found, already purchased), don't retry
        if (strpos($result['message'], 'not found') !== false || 
            strpos($result['message'], 'already') !== false) {
            return $result;
        }
        
        // Wait before retry (exponential backoff: 1s, 2s, 4s)
        if ($attempt < $max_retries) {
            $wait_time = pow(2, $attempt - 1);
            usleep($wait_time * 1000000); // Convert to microseconds
        }
    }
    
    // All retries failed
    $failure_log = [
        'timestamp' => date('Y-m-d H:i:s'),
        'action' => 'track_purchase_all_retries_failed',
        'user_id' => $user_id,
        'track_id' => $track_id,
        'payment_intent_id' => $payment_intent_id,
        'attempts' => $attempt,
        'last_error' => $last_error
    ];
    $log_file = __DIR__ . '/../logs/track_purchase_retries.log';
    file_put_contents($log_file, json_encode($failure_log) . "\n", FILE_APPEND | LOCK_EX);
    
    return ['success' => false, 'message' => "Failed after $attempt attempts: $last_error", 'purchase_id' => null];
}

/**
 * Schedule a purchase retry for later processing
 * This creates a record that can be processed by a cron job
 */
function schedulePurchaseRetry($payment_intent_id, $purchase_type, $metadata) {
    $retry_file = __DIR__ . '/../logs/purchase_retry_queue.log';
    $retry_entry = [
        'timestamp' => date('Y-m-d H:i:s'),
        'payment_intent_id' => $payment_intent_id,
        'purchase_type' => $purchase_type,
        'metadata' => $metadata,
        'retry_count' => 0,
        'max_retries' => 5,
        'next_retry_at' => date('Y-m-d H:i:s', strtotime('+5 minutes'))
    ];
    file_put_contents($retry_file, json_encode($retry_entry) . "\n", FILE_APPEND | LOCK_EX);
}

// Add credits to user account
function addCreditsToUser($user_id, $credits, $package, $subscription_period, $payment_intent_id) {
    require_once __DIR__ . '/../config/database.php';
    
    try {
        $pdo = getDBConnection();
        
        // Premium credits (500) do NOT expire - set expiration to NULL
        // Other packages (starter, pro) expire in 30 days
        $is_premium = ($package === 'premium');
        $expiration_date = $is_premium ? null : date('Y-m-d H:i:s', strtotime('+30 days'));
        
        // Get payment amount from payment intent if available
        $amount = 0;
        if (isset($payment_intent_id)) {
            // Try to get amount from payment intent metadata or calculate from package
            $package_prices = [
                'starter' => 19.99,
                'pro' => 59.00,
                'premium' => 129.00
            ];
            $amount = $package_prices[$package] ?? 0;
        }
        
        // Start transaction
        $pdo->beginTransaction();
        
        // CRITICAL: Check if user has an active subscription - if so, preserve their subscription plan
        // Credits and subscription plans are INDEPENDENT - adding credits should NEVER change subscription plan
        require_once __DIR__ . '/../utils/subscription_helpers.php';
        
        // Get current user plan FIRST
        $current_user = $pdo->prepare("SELECT plan FROM users WHERE id = ?");
        $current_user->execute([$user_id]);
        $user_data = $current_user->fetch(PDO::FETCH_ASSOC);
        $current_plan = $user_data['plan'] ?? null;
        
        // Check for active subscription
        $has_active_subscription = false;
        $subscription_plan = null;
        try {
            $has_active_subscription = hasActiveSubscription($user_id);
            if ($has_active_subscription) {
                $subscription_plan = $has_active_subscription['plan_name'] ?? null;
            }
        } catch (Exception $e) {
            error_log("addCreditsToUser: Error checking subscription for user {$user_id}: " . $e->getMessage());
        }
        
        // Also check user_subscriptions table directly as backup
        if (!$subscription_plan) {
            try {
                $sub_stmt = $pdo->prepare("
                    SELECT plan_name FROM user_subscriptions 
                    WHERE user_id = ? AND status IN ('active', 'trialing') 
                    AND current_period_end > NOW()
                    ORDER BY created_at DESC LIMIT 1
                ");
                $sub_stmt->execute([$user_id]);
                $sub_result = $sub_stmt->fetch(PDO::FETCH_ASSOC);
                if ($sub_result && !empty($sub_result['plan_name'])) {
                    $subscription_plan = $sub_result['plan_name'];
                    $has_active_subscription = true;
                }
            } catch (Exception $e) {
                error_log("addCreditsToUser: Error checking user_subscriptions for user {$user_id}: " . $e->getMessage());
            }
        }
        
        // DEFENSIVE: If user has a subscription plan (essential, pro, etc.), NEVER change it
        // Only subscription plans: essential, pro, premium, enterprise, free
        // Credit packages: starter, pro, premium (these are DIFFERENT from subscription plans)
        $subscription_plans = ['essential', 'pro', 'premium', 'enterprise', 'free'];
        $is_subscription_plan = in_array(strtolower($current_plan ?? ''), $subscription_plans);
        
        // Determine what plan to use
        if ($has_active_subscription && $subscription_plan) {
            // User has active subscription - ALWAYS preserve subscription plan
            $plan_name = $subscription_plan;
            error_log("addCreditsToUser: User {$user_id} has active subscription '{$plan_name}', preserving plan instead of changing to '{$package}'");
        } elseif ($is_subscription_plan && $current_plan) {
            // User's current plan is a subscription plan - preserve it even if subscription check failed
            $plan_name = $current_plan;
            error_log("addCreditsToUser: User {$user_id} has subscription plan '{$current_plan}', preserving it instead of changing to '{$package}' (subscription check may have failed)");
        } else {
            // No subscription found - only then update plan to package (for credit-only users)
            $plan_name = $package;
            error_log("addCreditsToUser: User {$user_id} has no active subscription, setting plan to package '{$package}'");
        }
        
        // Premium credits (500) do NOT expire - set expiration to NULL
        // Other packages (starter, pro) expire in 30 days
        // According to terms: Credits include commercial licensing rights for content created using those credits
        // Rights are permanent - once content is created with credits, commercial rights do not expire
        if ($is_premium) {
            // Premium credits don't expire - only update credits and plan, don't touch subscription_expires
            $stmt = $pdo->prepare("
                UPDATE users 
                SET credits = credits + ?, 
                    plan = ?
                WHERE id = ?
            ");
            $stmt->execute([$credits, $plan_name, $user_id]);
        } else {
            // Starter and Pro packages expire in 30 days
            $stmt = $pdo->prepare("
                UPDATE users 
                SET credits = credits + ?, 
                    plan = ?,
                    subscription_expires = ?
                WHERE id = ?
            ");
            $stmt->execute([$credits, $plan_name, $expiration_date, $user_id]);
        }
        
        // Log the purchase in credit_purchases table
        // Premium: expires_at = far future date (2099-12-31) since column doesn't allow NULL
        // Others: expires_at = 30 days from now
        // If expiration_date is NULL (premium), use far future date instead
        if ($expiration_date === null) {
            $expiration_date = '2099-12-31 23:59:59'; // Far future date for premium (never expires)
        }
        
        $stmt = $pdo->prepare("
            INSERT INTO credit_purchases 
            (user_id, package, credits, amount, payment_intent_id, expires_at, created_at) 
            VALUES (?, ?, ?, ?, ?, ?, NOW())
        ");
        $stmt->execute([$user_id, $package, $credits, $amount, $payment_intent_id, $expiration_date]);
        
        // Commit transaction
        $pdo->commit();
        
        // Log the credit addition
        // According to terms: Credits include commercial licensing rights for content created using those credits
        // Rights are permanent - once content is created with credits, commercial rights do not expire
        $credit_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'add_credits_to_user',
            'user_id' => $user_id,
            'credits_added' => $credits,
            'package' => $package,
            'plan_updated_to' => $plan_name,
            'subscription_period' => $subscription_period,
            'expiration_date' => $expiration_date,
            'never_expires' => $is_premium,
            'commercial_rights' => 'Credits include commercial licensing rights per terms section 5.2',
            'payment_intent_id' => $payment_intent_id,
            'status' => 'success'
        ];
        
        $credit_log_file = __DIR__ . '/../logs/user_credits.log';
        file_put_contents($credit_log_file, json_encode($credit_log) . "\n", FILE_APPEND | LOCK_EX);
        
    } catch (Exception $e) {
        // Rollback on error
        if (isset($pdo) && $pdo->inTransaction()) {
            $pdo->rollBack();
        }
        
        // Log error
        $error_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'add_credits_to_user_error',
            'user_id' => $user_id,
            'package' => $package,
            'credits' => $credits,
            'error' => $e->getMessage(),
            'payment_intent_id' => $payment_intent_id
        ];
        
        $error_log_file = __DIR__ . '/../logs/user_credits_errors.log';
        file_put_contents($error_log_file, json_encode($error_log) . "\n", FILE_APPEND | LOCK_EX);
        
        // Re-throw to allow caller to handle
        throw $e;
    }
}

// Process track purchase after successful payment (with invoice email)
// Returns: ['success' => bool, 'message' => string, 'purchase_id' => int|null]
function processTrackPurchase($user_id, $track_id, $credits_used, $payment_intent_id, $send_invoice = true) {
    require_once __DIR__ . '/../config/database.php';
    
    // Log function entry
    $entry_log = [
        'timestamp' => date('Y-m-d H:i:s'),
        'action' => 'processTrackPurchase_start',
        'user_id' => $user_id,
        'track_id' => $track_id,
        'credits_used' => $credits_used,
        'payment_intent_id' => $payment_intent_id,
        'send_invoice' => $send_invoice
    ];
    $entry_log_file = __DIR__ . '/../logs/track_purchase_detailed.log';
    file_put_contents($entry_log_file, json_encode($entry_log) . "\n", FILE_APPEND | LOCK_EX);
    
    try {
        $pdo = getDBConnection();
        
        // IDEMPOTENCY CHECK: If purchase already exists, return success (webhook might be called twice)
        $stmt = $pdo->prepare("
            SELECT id FROM track_purchases 
            WHERE user_id = ? AND track_id = ? AND stripe_payment_intent_id = ?
        ");
        $stmt->execute([$user_id, $track_id, $payment_intent_id]);
        $existing_purchase = $stmt->fetch();
        
        if ($existing_purchase) {
            $idempotency_log = [
                'timestamp' => date('Y-m-d H:i:s'),
                'action' => 'processTrackPurchase_already_purchased_idempotent',
                'user_id' => $user_id,
                'track_id' => $track_id,
                'existing_purchase_id' => $existing_purchase['id'],
                'payment_intent_id' => $payment_intent_id,
                'note' => 'Purchase already exists - idempotent success'
            ];
            file_put_contents($entry_log_file, json_encode($idempotency_log) . "\n", FILE_APPEND | LOCK_EX);
            return ['success' => true, 'message' => 'Purchase already exists (idempotent)', 'purchase_id' => $existing_purchase['id']];
        }
        
        // Get track information
        $stmt = $pdo->prepare("
            SELECT 
                mt.id,
                mt.title,
                mt.audio_url,
                mt.price,
                mt.user_id as artist_id,
                u.name as artist_name
            FROM music_tracks mt
            JOIN users u ON mt.user_id = u.id
            WHERE mt.id = ? AND mt.status = 'complete'
        ");
        
        $stmt->execute([$track_id]);
        $track = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$track) {
            $error_log = [
                'timestamp' => date('Y-m-d H:i:s'),
                'action' => 'processTrackPurchase_track_not_found',
                'user_id' => $user_id,
                'track_id' => $track_id,
                'payment_intent_id' => $payment_intent_id
            ];
            file_put_contents($entry_log_file, json_encode($error_log) . "\n", FILE_APPEND | LOCK_EX);
            return ['success' => false, 'message' => 'Track not found', 'purchase_id' => null];
        }
        
        // Log track found
        $track_found_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'processTrackPurchase_track_found',
            'user_id' => $user_id,
            'track_id' => $track_id,
            'track_title' => $track['title'],
            'track_price' => $track['price'],
            'artist_id' => $track['artist_id'],
            'payment_intent_id' => $payment_intent_id
        ];
        file_put_contents($entry_log_file, json_encode($track_found_log) . "\n", FILE_APPEND | LOCK_EX);
        
        // Start transaction
        $pdo->beginTransaction();
        
        try {
            // Determine revenue recipient (free user tracks go to platform, paid tracks go to artist)
            $is_free_user_track = false;
            $artist_stmt = $pdo->prepare("SELECT plan FROM users WHERE id = ?");
            $artist_stmt->execute([$track['artist_id']]);
            $artist = $artist_stmt->fetch(PDO::FETCH_ASSOC);
            if ($artist && strtolower($artist['plan']) === 'free') {
                $is_free_user_track = true;
            }
            $revenue_recipient = $is_free_user_track ? 'platform' : 'artist';
            $recipient_id = $is_free_user_track ? 1 : $track['artist_id'];
            
            // Record sale
            $stmt = $pdo->prepare("
                INSERT INTO sales (
                    track_id, buyer_id, artist_id, amount, quantity, 
                    revenue_recipient, recipient_id, is_free_user_track, 
                    created_at
                ) VALUES (?, ?, ?, ?, 1, ?, ?, ?, NOW())
            ");
            $stmt->execute([
                $track_id,
                $user_id,
                $track['artist_id'],
                $track['price'],
                $revenue_recipient,
                $recipient_id,
                $is_free_user_track ? 1 : 0
            ]);
            
            // Record the purchase
            $stmt = $pdo->prepare("
                INSERT INTO track_purchases (user_id, track_id, price_paid, credits_used, payment_method, stripe_payment_intent_id)
                VALUES (?, ?, ?, ?, 'stripe', ?)
            ");
            $stmt->execute([$user_id, $track_id, $track['price'], $credits_used, $payment_intent_id]);
            $purchase_id = $pdo->lastInsertId();
            
            // Log purchase record created
            $purchase_log = [
                'timestamp' => date('Y-m-d H:i:s'),
                'action' => 'processTrackPurchase_purchase_recorded',
                'user_id' => $user_id,
                'track_id' => $track_id,
                'purchase_id' => $purchase_id,
                'price_paid' => $track['price'],
                'payment_intent_id' => $payment_intent_id
            ];
            file_put_contents($entry_log_file, json_encode($purchase_log) . "\n", FILE_APPEND | LOCK_EX);
            
            // Add to user's library
            $stmt = $pdo->prepare("
                INSERT IGNORE INTO user_library (user_id, track_id, purchase_date)
                VALUES (?, ?, NOW())
            ");
            $stmt->execute([$user_id, $track_id]);
            
            // Log library addition
            $library_log = [
                'timestamp' => date('Y-m-d H:i:s'),
                'action' => 'processTrackPurchase_library_added',
                'user_id' => $user_id,
                'track_id' => $track_id,
                'payment_intent_id' => $payment_intent_id
            ];
            file_put_contents($entry_log_file, json_encode($library_log) . "\n", FILE_APPEND | LOCK_EX);
            
            // Record credit transaction only if credits were used
            if ($credits_used > 0) {
                $stmt = $pdo->prepare("
                    INSERT INTO credit_transactions (user_id, amount, type, description, stripe_payment_intent_id)
                    VALUES (?, ?, 'usage', ?, ?)
                ");
                $stmt->execute([
                    $user_id, 
                    -$credits_used, 
                    "Purchased track: {$track['title']}",
                    $payment_intent_id
                ]);
            }
            
            // Commit transaction
            $pdo->commit();
            
            // Notify artist of the purchase
            require_once __DIR__ . '/../utils/artist_notifications.php';
            notifyArtistOfTrackPurchase(
                $track['artist_id'],
                $track_id,
                $user_id,
                $track['price'],
                $purchase_id
            );
            
            // Log transaction committed
            $commit_log = [
                'timestamp' => date('Y-m-d H:i:s'),
                'action' => 'processTrackPurchase_transaction_committed',
                'user_id' => $user_id,
                'track_id' => $track_id,
                'purchase_id' => $purchase_id,
                'payment_intent_id' => $payment_intent_id
            ];
            file_put_contents($entry_log_file, json_encode($commit_log) . "\n", FILE_APPEND | LOCK_EX);
            
            // Send invoice email if requested
            $email_sent = false;
            if ($send_invoice) {
                // Get user information for invoice
                $user_stmt = $pdo->prepare("SELECT name, email FROM users WHERE id = ?");
                $user_stmt->execute([$user_id]);
                $user = $user_stmt->fetch(PDO::FETCH_ASSOC);
                
                // Get billing address from payment intent metadata if available
                $billing_address = [];
                try {
                    // Try to get billing address from Stripe payment intent
                    $stripe_secret = getStripeSecretKey();
                    $ch = curl_init();
                    curl_setopt($ch, CURLOPT_URL, 'https://api.stripe.com/v1/payment_intents/' . $payment_intent_id);
                    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $stripe_secret]);
                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                    $stripe_response = curl_exec($ch);
                    curl_close($ch);
                    
                    if ($stripe_response) {
                        $stripe_data = json_decode($stripe_response, true);
                        if (isset($stripe_data['charges']['data'][0]['billing_details'])) {
                            $billing = $stripe_data['charges']['data'][0]['billing_details'];
                            $billing_address = [
                                'billing_first_name' => explode(' ', $billing['name'] ?? '')[0] ?? '',
                                'billing_last_name' => explode(' ', $billing['name'] ?? '', 2)[1] ?? '',
                                'billing_email' => $billing['email'] ?? $user['email'],
                                'billing_address' => $billing['address']['line1'] ?? '',
                                'billing_city' => $billing['address']['city'] ?? '',
                                'billing_state' => $billing['address']['state'] ?? '',
                                'billing_zip' => $billing['address']['postal_code'] ?? '',
                                'billing_country' => $billing['address']['country'] ?? ''
                            ];
                        }
                    }
                } catch (Exception $e) {
                    error_log("Could not fetch billing address from Stripe: " . $e->getMessage());
                }
                
                // Send invoice email
                if ($user) {
                    require_once __DIR__ . '/../config/email.php';
                    $invoice_data = generateInvoiceEmail($user['name'], $user['email'], [
                        'purchase_id' => $purchase_id,
                        'track_id' => $track_id,
                        'track_ids' => [$track_id],
                        'price_paid' => $track['price'],
                        'total_amount' => $track['price'],
                        'payment_method' => 'stripe',
                        'payment_intent_id' => $payment_intent_id,
                        'purchase_date' => date('Y-m-d H:i:s'),
                        'billing_address' => $billing_address
                    ]);
                    
                    $email_sent = sendEmail(
                        $user['email'],
                        $user['name'],
                        $invoice_data['subject'],
                        $invoice_data['html'],
                        $invoice_data['text'],
                        'invoice',
                        $user_id,
                        $purchase_id
                    );
                    
                    if ($email_sent) {
                        error_log("Invoice email sent successfully to: " . $user['email']);
                    } else {
                        error_log("Failed to send invoice email to: " . $user['email']);
                    }
                }
            }
            
            // Log successful track purchase
            $track_purchase_log = [
                'timestamp' => date('Y-m-d H:i:s'),
                'action' => 'track_purchase_completed',
                'user_id' => $user_id,
                'track_id' => $track_id,
                'track_title' => $track['title'],
                'artist_name' => $track['artist_name'],
                'credits_used' => $credits_used,
                'payment_intent_id' => $payment_intent_id,
                'invoice_sent' => $email_sent
            ];
            
            $track_log_file = __DIR__ . '/../logs/track_purchases.log';
            file_put_contents($track_log_file, json_encode($track_purchase_log) . "\n", FILE_APPEND | LOCK_EX);
            
            return ['success' => true, 'message' => 'Purchase completed successfully', 'purchase_id' => $purchase_id];
            
        } catch (Exception $e) {
            $pdo->rollBack();
            throw $e;
        }
        
    } catch (Exception $e) {
        // Log error
        $error_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'track_purchase_error',
            'user_id' => $user_id,
            'track_id' => $track_id,
            'error' => $e->getMessage(),
            'trace' => $e->getTraceAsString(),
            'payment_intent_id' => $payment_intent_id
        ];
        
        $error_log_file = __DIR__ . '/../logs/track_purchase_errors.log';
        file_put_contents($error_log_file, json_encode($error_log) . "\n", FILE_APPEND | LOCK_EX);
        
        // Return failure status so caller can retry
        return ['success' => false, 'message' => $e->getMessage(), 'purchase_id' => null];
    }
}

// Process mixed cart payment (credits + tracks)
function processMixedCartPayment($paymentIntent, $metadata) {
    require_once __DIR__ . '/../config/database.php';
    
    try {
        $pdo = getDBConnection();
        $user_id = $metadata['user_id'] ?? null;
        
        // Parse cart_items from metadata - Stripe stores metadata as strings
        $cart_items_json = $metadata['cart_items'] ?? '[]';
        if (is_string($cart_items_json)) {
            $cart_items = json_decode($cart_items_json, true);
        } else {
            $cart_items = $cart_items_json;
        }
        
        // If cart_items is still not an array, try to get it from the payment intent metadata directly
        if (!is_array($cart_items) || empty($cart_items)) {
            $cart_items_json = $paymentIntent->metadata->cart_items ?? '[]';
            if (is_string($cart_items_json)) {
                $cart_items = json_decode($cart_items_json, true);
            } else {
                $cart_items = $cart_items_json ?? [];
            }
        }
        
        $total_credits = $metadata['total_credits'] ?? 0;
        $subscription_period = $metadata['subscription_period'] ?? '30_days';
        
        if (!$user_id) {
            throw new Exception('User ID missing from payment metadata');
        }
        
        if (empty($cart_items) || !is_array($cart_items)) {
            // Log warning but don't throw - might be a credit-only purchase
            $warning_log = [
                'timestamp' => date('Y-m-d H:i:s'),
                'action' => 'processMixedCartPayment_warning',
                'payment_intent_id' => $paymentIntent->id,
                'user_id' => $user_id,
                'warning' => 'No cart items found in metadata',
                'metadata' => $metadata
            ];
            $warning_log_file = __DIR__ . '/../logs/mixed_cart_payments.log';
            file_put_contents($warning_log_file, json_encode($warning_log) . "\n", FILE_APPEND | LOCK_EX);
            
            // If there are credits to add, still process them
            if ($total_credits > 0) {
                // Try to get package from metadata
                $package = $metadata['package'] ?? 'unknown';
                addCreditsToUser($user_id, $total_credits, $package, $subscription_period, $paymentIntent->id);
            }
            return;
        }
        
        // Log mixed cart processing
        $mixed_cart_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'processMixedCartPayment',
            'payment_intent_id' => $paymentIntent->id,
            'user_id' => $user_id,
            'total_credits' => $total_credits,
            'cart_items_count' => count($cart_items),
            'cart_items' => $cart_items
        ];
        
        $mixed_cart_log_file = __DIR__ . '/../logs/mixed_cart_payments.log';
        file_put_contents($mixed_cart_log_file, json_encode($mixed_cart_log) . "\n", FILE_APPEND | LOCK_EX);
        
        // Track purchased track IDs for invoice
        $purchased_track_ids = [];
        $total_track_amount = 0;
        
        // Process each cart item with comprehensive logging
        $processed_items = [];
        $failed_items = [];
        
        foreach ($cart_items as $index => $item) {
            $item_log = [
                'timestamp' => date('Y-m-d H:i:s'),
                'action' => 'processing_cart_item',
                'payment_intent_id' => $paymentIntent->id,
                'user_id' => $user_id,
                'item_index' => $index,
                'item' => $item
            ];
            
            if (!isset($item['type'])) {
                $item_log['status'] = 'failed';
                $item_log['error'] = 'Item missing type field';
                $failed_items[] = $item_log;
                
                $log_file = __DIR__ . '/../logs/mixed_cart_item_errors.log';
                file_put_contents($log_file, json_encode($item_log) . "\n", FILE_APPEND | LOCK_EX);
                error_log("processMixedCartPayment: Item missing type: " . json_encode($item));
                continue;
            }
            
            try {
                // Handle both full format (type, credits, package) and minimal format (t, i, q, a)
                $item_type = $item['type'] ?? $item['t'] ?? null;
                
                if ($item_type === 'credit') {
                    // Add credits to user
                    // Handle both full and minimal formats
                    $package = $item['package'] ?? $item['i'] ?? null;
                    $credits = $item['credits'] ?? null;
                    $quantity = $item['quantity'] ?? $item['q'] ?? 1;
                    
                    // If credits not provided, calculate from package
                    if (!$credits && $package) {
                        $package_credits_map = [
                            'starter' => 30,
                            'pro' => 150,
                            'premium' => 500
                        ];
                        $credits = ($package_credits_map[$package] ?? 0) * $quantity;
                    }
                    
                    if ($credits && $credits > 0 && $package) {
                        $item_log['action'] = 'processing_credit_item';
                        $item_log['package'] = $package;
                        $item_log['credits'] = $credits;
                        $item_log['quantity'] = $quantity;
                        $log_file = __DIR__ . '/../logs/mixed_cart_processing.log';
                        file_put_contents($log_file, json_encode($item_log) . "\n", FILE_APPEND | LOCK_EX);
                        
                        addCreditsToUser($user_id, $credits, $package, $subscription_period, $paymentIntent->id);
                        
                        $item_log['status'] = 'success';
                        $processed_items[] = $item_log;
                    } else {
                        $item_log['status'] = 'failed';
                        $item_log['error'] = 'Credit item missing credits or package. Package: ' . ($package ?? 'null') . ', Credits: ' . ($credits ?? 'null');
                        $failed_items[] = $item_log;
                        
                        $log_file = __DIR__ . '/../logs/mixed_cart_item_errors.log';
                        file_put_contents($log_file, json_encode($item_log) . "\n", FILE_APPEND | LOCK_EX);
                        error_log("processMixedCartPayment: Credit item missing required fields: " . json_encode($item));
                    }
                } elseif ($item['type'] === 'ticket' || (isset($item['t']) && $item['t'] === 'ticket')) {
                    // Process ticket purchase - handle both full and minimal formats
                    $event_id = $item['event_id'] ?? $item['i'] ?? null;
                    $quantity = $item['quantity'] ?? $item['q'] ?? 1;
                    
                    if (!$event_id) {
                        $item_log['status'] = 'failed';
                        $item_log['error'] = 'Ticket item missing event_id';
                        $failed_items[] = $item_log;
                        continue;
                    }
                    
                    $item_log['action'] = 'processing_ticket_item';
                    $item_log['event_id'] = $event_id;
                    $item_log['quantity'] = $quantity;
                    $log_file = __DIR__ . '/../logs/mixed_cart_processing.log';
                    file_put_contents($log_file, json_encode($item_log) . "\n", FILE_APPEND | LOCK_EX);
                    
                    try {
                        // Process ticket purchase
                        processEventTicketPurchase($user_id, $event_id, $paymentIntent, $quantity);
                        
                        $item_log['status'] = 'success';
                        $processed_items[] = $item_log;
                    } catch (Exception $e) {
                        $item_log['status'] = 'failed';
                        $item_log['error'] = $e->getMessage();
                        $failed_items[] = $item_log;
                        error_log("processMixedCartPayment: Ticket purchase failed: " . $e->getMessage());
                    }
                } elseif ($item['type'] === 'track') {
                    // Process track purchase (but don't send invoice yet - we'll send one for all tracks)
                    if (isset($item['track_id'])) {
                        $item_log['action'] = 'processing_track_item';
                        $item_log['track_id'] = $item['track_id'];
                        $log_file = __DIR__ . '/../logs/mixed_cart_processing.log';
                        file_put_contents($log_file, json_encode($item_log) . "\n", FILE_APPEND | LOCK_EX);
                        
                        // Log BEFORE attempting purchase
                        $before_log = [
                            'timestamp' => date('Y-m-d H:i:s'),
                            'action' => 'before_track_purchase',
                            'payment_intent_id' => $paymentIntent->id,
                            'user_id' => $user_id,
                            'track_id' => $item['track_id'],
                            'item' => $item
                        ];
                        $before_log_file = __DIR__ . '/../logs/track_purchase_detailed.log';
                        file_put_contents($before_log_file, json_encode($before_log) . "\n", FILE_APPEND | LOCK_EX);
                        
                        $credits_used = $item['credits_used'] ?? 0;
                        
                        try {
                            // Process purchase without sending email (we'll send one invoice for all tracks)
                            $purchase_result = processTrackPurchase($user_id, $item['track_id'], $credits_used, $paymentIntent->id, false);
                            
                            if ($purchase_result['success']) {
                                // Log AFTER successful purchase
                                $after_log = [
                                    'timestamp' => date('Y-m-d H:i:s'),
                                    'action' => 'after_track_purchase_success',
                                    'payment_intent_id' => $paymentIntent->id,
                                    'user_id' => $user_id,
                                    'track_id' => $item['track_id'],
                                    'status' => 'success',
                                    'purchase_id' => $purchase_result['purchase_id'] ?? null
                                ];
                                file_put_contents($before_log_file, json_encode($after_log) . "\n", FILE_APPEND | LOCK_EX);
                                
                                $purchased_track_ids[] = $item['track_id'];
                                $item_log['status'] = 'success';
                                $processed_items[] = $item_log;
                            } else {
                                // Purchase failed
                                throw new Exception($purchase_result['message']);
                            }
                            
                            // Get track price from database
                            $track_stmt = $pdo->prepare("SELECT price FROM music_tracks WHERE id = ?");
                            $track_stmt->execute([$item['track_id']]);
                            $track_price = $track_stmt->fetch(PDO::FETCH_ASSOC);
                            if ($track_price) {
                                $total_track_amount += floatval($track_price['price']);
                            }
                        } catch (Exception $e) {
                            // Log the exception
                            $error_log = [
                                'timestamp' => date('Y-m-d H:i:s'),
                                'action' => 'after_track_purchase_error',
                                'payment_intent_id' => $paymentIntent->id,
                                'user_id' => $user_id,
                                'track_id' => $item['track_id'],
                                'error' => $e->getMessage(),
                                'trace' => $e->getTraceAsString(),
                                'item' => $item
                            ];
                            $error_log_file = __DIR__ . '/../logs/track_purchase_detailed_errors.log';
                            file_put_contents($error_log_file, json_encode($error_log) . "\n", FILE_APPEND | LOCK_EX);
                            
                            $item_log['status'] = 'failed';
                            $item_log['error'] = $e->getMessage();
                            $failed_items[] = $item_log;
                            
                            // Continue processing other items even if one fails
                            error_log("processMixedCartPayment: Failed to process track {$item['track_id']}: " . $e->getMessage());
                        }
                    } else {
                        $item_log['status'] = 'failed';
                        $item_log['error'] = 'Track item missing track_id';
                        $failed_items[] = $item_log;
                        
                        $log_file = __DIR__ . '/../logs/mixed_cart_item_errors.log';
                        file_put_contents($log_file, json_encode($item_log) . "\n", FILE_APPEND | LOCK_EX);
                        error_log("processMixedCartPayment: Track item missing track_id: " . json_encode($item));
                    }
                } else {
                    $item_log['status'] = 'failed';
                    $item_log['error'] = 'Unknown item type: ' . $item['type'];
                    $failed_items[] = $item_log;
                    
                    $log_file = __DIR__ . '/../logs/mixed_cart_item_errors.log';
                    file_put_contents($log_file, json_encode($item_log) . "\n", FILE_APPEND | LOCK_EX);
                }
            } catch (Exception $e) {
                $item_log['status'] = 'failed';
                $item_log['error'] = $e->getMessage();
                $item_log['trace'] = $e->getTraceAsString();
                $failed_items[] = $item_log;
                
                $log_file = __DIR__ . '/../logs/mixed_cart_item_errors.log';
                file_put_contents($log_file, json_encode($item_log) . "\n", FILE_APPEND | LOCK_EX);
            }
        }
        
        // Log final summary
        $summary_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'processMixedCartPayment_summary',
            'payment_intent_id' => $paymentIntent->id,
            'user_id' => $user_id,
            'total_items' => count($cart_items),
            'processed_count' => count($processed_items),
            'failed_count' => count($failed_items),
            'purchased_track_ids' => $purchased_track_ids,
            'processed_items' => $processed_items,
            'failed_items' => $failed_items
        ];
        
        $summary_log_file = __DIR__ . '/../logs/mixed_cart_summary.log';
        file_put_contents($summary_log_file, json_encode($summary_log) . "\n", FILE_APPEND | LOCK_EX);
        
        // If there were failures, log an alert
        if (!empty($failed_items)) {
            $alert_log = [
                'timestamp' => date('Y-m-d H:i:s'),
                'action' => 'ALERT_purchase_failures',
                'payment_intent_id' => $paymentIntent->id,
                'user_id' => $user_id,
                'failed_count' => count($failed_items),
                'failed_items' => $failed_items,
                'total_items' => count($cart_items),
                'severity' => 'HIGH'
            ];
            
            $alert_log_file = __DIR__ . '/../logs/purchase_failure_alerts.log';
            file_put_contents($alert_log_file, json_encode($alert_log) . "\n", FILE_APPEND | LOCK_EX);
        }
        
        // VALIDATION: Verify purchases match cart after processing
        try {
            require_once __DIR__ . '/purchase_validation.php';
            $validation = validatePurchase($paymentIntent->id, $user_id);
            
            if (!$validation['valid']) {
                // Log validation failure
                $validation_alert = [
                    'timestamp' => date('Y-m-d H:i:s'),
                    'action' => 'ALERT_purchase_validation_failed',
                    'payment_intent_id' => $paymentIntent->id,
                    'user_id' => $user_id,
                    'issues' => $validation['issues'],
                    'expected_tracks' => $validation['expected'] ?? [],
                    'actual_tracks' => $validation['actual'] ?? [],
                    'severity' => 'CRITICAL'
                ];
                
                $validation_log_file = __DIR__ . '/../logs/purchase_validation_failures.log';
                file_put_contents($validation_log_file, json_encode($validation_alert) . "\n", FILE_APPEND | LOCK_EX);
                
                // Also add to general alerts
                $alert_log_file = __DIR__ . '/../logs/purchase_failure_alerts.log';
                file_put_contents($alert_log_file, json_encode($validation_alert) . "\n", FILE_APPEND | LOCK_EX);
                
                error_log("CRITICAL: Purchase validation failed for payment_intent_id: {$paymentIntent->id}. Issues: " . implode(', ', $validation['issues']));
            } else {
                // Log successful validation
                $validation_success = [
                    'timestamp' => date('Y-m-d H:i:s'),
                    'action' => 'purchase_validation_passed',
                    'payment_intent_id' => $paymentIntent->id,
                    'user_id' => $user_id
                ];
                $validation_log_file = __DIR__ . '/../logs/purchase_validation_success.log';
                file_put_contents($validation_log_file, json_encode($validation_success) . "\n", FILE_APPEND | LOCK_EX);
            }
        } catch (Exception $e) {
            error_log("Warning: Purchase validation check failed: " . $e->getMessage());
        }
        
        // Send invoice email if tracks were purchased
        if (!empty($purchased_track_ids)) {
            try {
                require_once __DIR__ . '/../config/email.php';
                
                // Get user information
                $user_stmt = $pdo->prepare("SELECT name, email FROM users WHERE id = ?");
                $user_stmt->execute([$user_id]);
                $user = $user_stmt->fetch(PDO::FETCH_ASSOC);
                
                // Get billing address from payment intent metadata
                $billing_address = [];
                try {
                    $stripe_secret = getStripeSecretKey();
                    $ch = curl_init();
                    curl_setopt($ch, CURLOPT_URL, 'https://api.stripe.com/v1/payment_intents/' . $paymentIntent->id);
                    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $stripe_secret]);
                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                    $stripe_response = curl_exec($ch);
                    curl_close($ch);
                    
                    if ($stripe_response) {
                        $stripe_data = json_decode($stripe_response, true);
                        if (isset($stripe_data['charges']['data'][0]['billing_details'])) {
                            $billing = $stripe_data['charges']['data'][0]['billing_details'];
                            $billing_address = [
                                'billing_first_name' => explode(' ', $billing['name'] ?? '')[0] ?? '',
                                'billing_last_name' => explode(' ', $billing['name'] ?? '', 2)[1] ?? '',
                                'billing_email' => $billing['email'] ?? ($user['email'] ?? ''),
                                'billing_address' => $billing['address']['line1'] ?? '',
                                'billing_city' => $billing['address']['city'] ?? '',
                                'billing_state' => $billing['address']['state'] ?? '',
                                'billing_zip' => $billing['address']['postal_code'] ?? '',
                                'billing_country' => $billing['address']['country'] ?? ''
                            ];
                        }
                        // Also check metadata for billing info
                        if (empty($billing_address) && isset($stripe_data['metadata'])) {
                            $meta = $stripe_data['metadata'];
                            if (isset($meta['billing_email'])) {
                                $billing_address = [
                                    'billing_first_name' => $meta['billing_name'] ?? '',
                                    'billing_last_name' => '',
                                    'billing_email' => $meta['billing_email'] ?? ($user['email'] ?? ''),
                                    'billing_address' => $meta['billing_address'] ?? '',
                                    'billing_city' => $meta['billing_city'] ?? '',
                                    'billing_state' => $meta['billing_state'] ?? '',
                                    'billing_zip' => $meta['billing_zip'] ?? '',
                                    'billing_country' => $meta['billing_country'] ?? ''
                                ];
                            }
                        }
                    }
                } catch (Exception $e) {
                    error_log("Could not fetch billing address from Stripe: " . $e->getMessage());
                }
                
                if ($user) {
                    $invoice_data = generateInvoiceEmail($user['name'], $user['email'], [
                        'purchase_id' => null, // Multiple purchases
                        'track_ids' => $purchased_track_ids,
                        'price_paid' => $total_track_amount,
                        'total_amount' => $total_track_amount,
                        'payment_method' => 'stripe',
                        'payment_intent_id' => $paymentIntent->id,
                        'purchase_date' => date('Y-m-d H:i:s'),
                        'billing_address' => $billing_address
                    ]);
                    
                    $email_sent = sendEmail(
                        $user['email'],
                        $user['name'],
                        $invoice_data['subject'],
                        $invoice_data['html'],
                        $invoice_data['text'],
                        'invoice',
                        $user_id,
                        $paymentIntent->id
                    );
                    
                    if ($email_sent) {
                        error_log("Invoice email sent successfully for mixed cart to: " . $user['email']);
                    } else {
                        error_log("Failed to send invoice email for mixed cart to: " . $user['email']);
                    }
                }
            } catch (Exception $e) {
                error_log("Error sending invoice email for mixed cart: " . $e->getMessage());
            }
        }
        
        // Log successful mixed cart processing
        $success_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'mixed_cart_payment_success',
            'payment_intent_id' => $paymentIntent->id,
            'user_id' => $user_id,
            'items_processed' => count($cart_items),
            'tracks_purchased' => count($purchased_track_ids),
            'invoice_sent' => !empty($purchased_track_ids) && ($email_sent ?? false)
        ];
        
        file_put_contents($mixed_cart_log_file, json_encode($success_log) . "\n", FILE_APPEND | LOCK_EX);
        
        // Return success status (check if any critical items failed)
        $has_critical_failures = !empty($failed_items);
        return [
            'success' => !$has_critical_failures,
            'message' => $has_critical_failures ? 'Some items failed to process' : 'All items processed successfully',
            'processed_count' => count($processed_items),
            'failed_count' => count($failed_items)
        ];
        
    } catch (Exception $e) {
        // Log mixed cart error
        $error_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'mixed_cart_payment_error',
            'payment_intent_id' => $paymentIntent->id,
            'user_id' => $user_id ?? 'unknown',
            'error' => $e->getMessage(),
            'trace' => $e->getTraceAsString()
        ];
        
        $error_log_file = __DIR__ . '/../logs/mixed_cart_errors.log';
        file_put_contents($error_log_file, json_encode($error_log) . "\n", FILE_APPEND | LOCK_EX);
        
        return ['success' => false, 'message' => $e->getMessage(), 'processed_count' => 0, 'failed_count' => 0];
    }
}

function handleFailedPayment($paymentIntent) {
    // Update your database with failed payment
    // Send failure notification
    // Update user subscription status
    
    // Example database update
    /*
    $db = new PDO('mysql:host=localhost;dbname=your_db', 'username', 'password');
    $stmt = $db->prepare("UPDATE payments SET status = 'failed', failure_reason = ?, processed_at = NOW() WHERE stripe_payment_intent_id = ?");
    $stmt->execute([$paymentIntent->last_payment_error->message ?? 'Unknown error', $paymentIntent->id]);
    */
    
    // Log the action
    $action_log = [
        'timestamp' => date('Y-m-d H:i:s'),
        'action' => 'handleFailedPayment',
        'payment_intent_id' => $paymentIntent->id,
        'failure_reason' => $paymentIntent->last_payment_error->message ?? 'Unknown error'
    ];
    
    $action_log_file = __DIR__ . '/../logs/stripe_actions.log';
    file_put_contents($action_log_file, json_encode($action_log) . "\n", FILE_APPEND | LOCK_EX);
}

function handleSubscriptionCreated($subscription) {
    require_once __DIR__ . '/../config/database.php';
    require_once __DIR__ . '/../utils/subscription_helpers.php';
    require_once __DIR__ . '/../includes/grace_period.php';
    require_once __DIR__ . '/../config/subscription_plans.php';
    
    // Load plans config once at function start
    $plans_config = require __DIR__ . '/../config/subscription_plans.php';
    
    try {
        $pdo = getDBConnection();
        
        // Get user by Stripe customer ID
        $customer_id = $subscription->customer;
        $stmt = $pdo->prepare("SELECT id FROM users WHERE stripe_customer_id = ?");
        $stmt->execute([$customer_id]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$user) {
            // Try to get user from metadata
            $metadata = $subscription->metadata ?? (object)[];
            $user_id = $metadata->user_id ?? null;
            
            if (!$user_id) {
                throw new Exception("User not found for customer: {$customer_id}");
            }
        } else {
            $user_id = $user['id'];
        }
        
        // Determine plan name from subscription items (improved detection)
        $plan_name = 'essential'; // Default
        $price_id = null;
        
        // Primary: Get plan from price ID (most reliable)
        if (isset($subscription->items->data[0]->price->id)) {
            $price_id = $subscription->items->data[0]->price->id;
            // Match price ID to plan in config
            foreach ($plans_config as $plan_key => $plan_data) {
                if (isset($plan_data['stripe_price_id']) && $plan_data['stripe_price_id'] === $price_id) {
                    $plan_name = $plan_key;
                    break;
                }
            }
        }
        
        // Fallback 1: Check price metadata
        if ($plan_name === 'essential' && isset($subscription->items->data[0]->price->metadata->plan_name)) {
            $plan_name = $subscription->items->data[0]->price->metadata->plan_name;
        }
        
        // Fallback 2: Check subscription metadata
        if ($plan_name === 'essential' && isset($subscription->metadata->plan)) {
            $plan_name = $subscription->metadata->plan;
        }
        
        // Log warning if plan not found (for admin alert)
        if ($plan_name === 'essential' && $price_id) {
            error_log("WARNING: Plan not found for price_id: {$price_id} (subscription: {$subscription->id}, user: {$user_id}). Defaulting to 'essential'. Admin should verify.");
        }
        $track_limit = 5; // Default
        if (isset($plans_config[$plan_name])) {
            $track_limit = $plans_config[$plan_name]['tracks_per_month'];
        }
        
        // HIGH PRIORITY FIX: Check for existing active subscriptions and cancel old ones
        // This prevents users from having multiple active subscriptions
        $existing_active_stmt = $pdo->prepare("
            SELECT id, stripe_subscription_id, status, created_at 
            FROM user_subscriptions 
            WHERE user_id = ? AND status IN ('active', 'trialing')
            AND stripe_subscription_id != ?
            ORDER BY created_at DESC
        ");
        $existing_active_stmt->execute([$user_id, $subscription->id]);
        $all_active_subs = $existing_active_stmt->fetchAll(PDO::FETCH_ASSOC);
        
        if (!empty($all_active_subs)) {
            error_log("User {$user_id} has multiple active subscriptions. Canceling old ones. New subscription: {$subscription->id}");
            
            // Cancel old subscriptions in Stripe
            $stripe_secret = getStripeSecretKey();
            
            foreach ($all_active_subs as $old_sub) {
                $old_stripe_id = $old_sub['stripe_subscription_id'];
                
                try {
                    // Cancel old subscription in Stripe
                    $cancel_ch = curl_init();
                    curl_setopt($cancel_ch, CURLOPT_URL, "https://api.stripe.com/v1/subscriptions/{$old_stripe_id}");
                    curl_setopt($cancel_ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $stripe_secret]);
                    curl_setopt($cancel_ch, CURLOPT_RETURNTRANSFER, true);
                    curl_setopt($cancel_ch, CURLOPT_POST, true);
                    curl_setopt($cancel_ch, CURLOPT_POSTFIELDS, http_build_query(['cancel_at_period_end' => 'false'])); // Cancel immediately
                    $cancel_response = curl_exec($cancel_ch);
                    $cancel_http = curl_getinfo($cancel_ch, CURLINFO_HTTP_CODE);
                    curl_close($cancel_ch);
                    
                    if ($cancel_http === 200) {
                        error_log("Canceled old subscription {$old_stripe_id} for user {$user_id} (new subscription: {$subscription->id})");
                    } else {
                        error_log("Failed to cancel old subscription {$old_stripe_id} in Stripe: HTTP {$cancel_http}");
                    }
                } catch (Exception $cancel_e) {
                    error_log("Error canceling old subscription {$old_stripe_id}: " . $cancel_e->getMessage());
                }
            }
        }
        
        $pdo->beginTransaction();
        
        // Create or update subscription record
        $stmt = $pdo->prepare("
            INSERT INTO user_subscriptions (
                user_id, stripe_subscription_id, stripe_customer_id, plan_name, status,
                current_period_start, current_period_end, created_at
            ) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
            ON DUPLICATE KEY UPDATE
                status = VALUES(status),
                current_period_start = VALUES(current_period_start),
                current_period_end = VALUES(current_period_end),
                updated_at = NOW()
        ");
        
        $period_start = date('Y-m-d H:i:s', $subscription->current_period_start);
        $period_end = date('Y-m-d H:i:s', $subscription->current_period_end);
        
        $stmt->execute([
            $user_id,
            $subscription->id,
            $customer_id,
            $plan_name,
            $subscription->status,
            $period_start,
            $period_end
        ]);
        
        // Update user plan and save Stripe customer ID if not already set
        $stmt = $pdo->prepare("UPDATE users SET plan = ?, stripe_customer_id = COALESCE(stripe_customer_id, ?) WHERE id = ?");
        $stmt->execute([$plan_name, $customer_id, $user_id]);
        
        // Initialize monthly track usage for current subscription period
        $period_start = date('Y-m-d H:i:s', $subscription->current_period_start);
        $year_month = date('Y-m', $subscription->current_period_start); // For backward compatibility
        
        // Get subscription ID
        $sub_stmt = $pdo->prepare("SELECT id FROM user_subscriptions WHERE stripe_subscription_id = ?");
        $sub_stmt->execute([$subscription->id]);
        $sub_record = $sub_stmt->fetch(PDO::FETCH_ASSOC);
        $subscription_id = $sub_record['id'] ?? null;
        
        $stmt = $pdo->prepare("
            INSERT INTO monthly_track_usage (
                user_id, subscription_id, subscription_period_start, 
                year_month, tracks_created, track_limit, reset_at
            )
            VALUES (?, ?, ?, ?, 0, ?, NOW())
            ON DUPLICATE KEY UPDATE 
                track_limit = VALUES(track_limit),
                reset_at = NOW()
        ");
        $stmt->execute([
            $user_id, 
            $subscription_id, 
            $period_start, 
            $year_month, 
            $track_limit
        ]);
        
        // Mark old active subscriptions as canceled in database
        if (!empty($all_active_subs)) {
            foreach ($all_active_subs as $old_sub) {
                $cancel_stmt = $pdo->prepare("UPDATE user_subscriptions SET status = 'canceled', updated_at = NOW() WHERE id = ?");
                $cancel_stmt->execute([$old_sub['id']]);
            }
            error_log("Marked " . count($all_active_subs) . " old subscriptions as canceled in database for user {$user_id}");
        }
        
        $pdo->commit();
        
        // ============================================================
        // GRACE PERIOD CONVERSION
        // If user upgrades within 30 days of account creation,
        // convert their grace-period-eligible tracks to commercial rights
        // ============================================================
        $grace_conversion_result = null;
        try {
            $grace_conversion_result = convertGracePeriodTracks($user_id);
            if ($grace_conversion_result['success'] && $grace_conversion_result['converted_count'] > 0) {
                error_log("Grace period conversion: User $user_id upgraded within 30 days, converted {$grace_conversion_result['converted_count']} tracks to commercial rights");
            }
        } catch (Exception $grace_e) {
            error_log("Grace period conversion warning (non-fatal): " . $grace_e->getMessage());
        }
        
        // Log success
        $action_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'handleSubscriptionCreated',
            'subscription_id' => $subscription->id,
            'customer_id' => $customer_id,
            'user_id' => $user_id,
            'plan_name' => $plan_name,
            'price_id' => $price_id,
            'status' => $subscription->status,
            'track_limit' => $track_limit,
            'old_subscriptions_canceled' => count($all_active_subs ?? []),
            'grace_period_conversion' => $grace_conversion_result
        ];
        
        $action_log_file = __DIR__ . '/../logs/stripe_actions.log';
        file_put_contents($action_log_file, json_encode($action_log) . "\n", FILE_APPEND | LOCK_EX);
        
    } catch (Exception $e) {
        if (isset($pdo) && $pdo->inTransaction()) {
            $pdo->rollBack();
        }
        
        $error_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'handleSubscriptionCreated_error',
            'subscription_id' => $subscription->id,
            'error' => $e->getMessage()
        ];
        
        $error_log_file = __DIR__ . '/../logs/stripe_actions.log';
        file_put_contents($error_log_file, json_encode($error_log) . "\n", FILE_APPEND | LOCK_EX);
        
        error_log("Subscription creation error: " . $e->getMessage());
    }
}

function handleSubscriptionUpdated($subscription) {
    require_once __DIR__ . '/../config/database.php';
    require_once __DIR__ . '/../includes/grace_period.php';
    
    try {
        $pdo = getDBConnection();
        
        // Get existing subscription record to detect changes (BEFORE updating)
        $existing_stmt = $pdo->prepare("SELECT id, user_id, plan_name, current_period_start FROM user_subscriptions WHERE stripe_subscription_id = ?");
        $existing_stmt->execute([$subscription->id]);
        $existing_sub = $existing_stmt->fetch(PDO::FETCH_ASSOC);
        
        // If subscription doesn't exist, log and return (shouldn't happen but handle gracefully)
        if (!$existing_sub) {
            error_log("Warning: Subscription {$subscription->id} not found in database during update webhook");
            return; // Exit early - subscription doesn't exist in our DB
        }
        
        // Store old values BEFORE updating
        $old_plan_name = $existing_sub['plan_name'];
        $old_period_start = $existing_sub['current_period_start'];
        $user_id = $existing_sub['user_id'];
        $subscription_db_id = $existing_sub['id'];
        
        // Determine plan name from subscription price
        $new_plan_name = 'essential'; // Default
        if (!empty($subscription->items->data[0]->price->id)) {
            $price_id = $subscription->items->data[0]->price->id;
            require_once __DIR__ . '/../config/subscription_plans.php';
            $plans_config = require __DIR__ . '/../config/subscription_plans.php';
            
            foreach ($plans_config as $plan_key => $plan_data) {
                if (isset($plan_data['stripe_price_id']) && $plan_data['stripe_price_id'] === $price_id) {
                    $new_plan_name = $plan_key;
                    break;
                }
            }
        }
        
        // Detect if plan changed (using OLD plan name)
        $plan_changed = false;
        if ($old_plan_name !== $new_plan_name) {
            $plan_changed = true;
            error_log("Plan change detected: {$old_plan_name} → {$new_plan_name} for subscription {$subscription->id} (user {$user_id})");
        }
        
        // Update subscription record
        $stmt = $pdo->prepare("
            UPDATE user_subscriptions
            SET status = ?,
                plan_name = ?,
                current_period_start = ?,
                current_period_end = ?,
                cancel_at_period_end = ?,
                updated_at = NOW()
            WHERE stripe_subscription_id = ?
        ");
        
        $period_start = date('Y-m-d H:i:s', $subscription->current_period_start);
        $period_end = date('Y-m-d H:i:s', $subscription->current_period_end);
        $cancel_at_period_end = $subscription->cancel_at_period_end ? 1 : 0;
        
        $stmt->execute([
            $subscription->status,
            $new_plan_name,
            $period_start,
            $period_end,
            $cancel_at_period_end,
            $subscription->id
        ]);
        
        // Update user plan based on subscription status
        if ($user_id) {
            if (in_array($subscription->status, ['active', 'trialing'])) {
                // Active subscription - update to new plan
                $user_update_stmt = $pdo->prepare("UPDATE users SET plan = ? WHERE id = ?");
                $user_update_stmt->execute([$new_plan_name, $user_id]);
                error_log("Updated user {$user_id} plan to {$new_plan_name} (subscription {$subscription->id})");
            } elseif (in_array($subscription->status, ['canceled', 'past_due', 'unpaid'])) {
                // Subscription canceled/refunded - set to 'free'
                $user_update_stmt = $pdo->prepare("UPDATE users SET plan = 'free' WHERE id = ?");
                $user_update_stmt->execute([$user_id]);
                
                // Also update subscription plan_name to 'free' for consistency
                $sub_update_stmt = $pdo->prepare("UPDATE user_subscriptions SET plan_name = 'free' WHERE id = ?");
                $sub_update_stmt->execute([$subscription_db_id]);
                
                error_log("Subscription canceled/refunded - Set user {$user_id} plan to 'free' (subscription {$subscription->id})");
            }
        }
        
        // If subscription renewed (new billing period started), reset monthly usage
        if ($subscription->status === 'active' && $user_id) {
            // Check if billing period actually changed (renewal occurred)
            $stripe_period_start = date('Y-m-d H:i:s', $subscription->current_period_start);
            $stripe_timestamp = $subscription->current_period_start; // Unix timestamp from Stripe
            
            // Convert old period start to comparable format
            $db_period_start = $old_period_start;
            $db_timestamp = null;
            
            if (is_numeric($db_period_start)) {
                $db_timestamp = $db_period_start;
                $db_period_start = date('Y-m-d H:i:s', $db_period_start);
            } elseif (is_string($db_period_start)) {
                // Already in datetime format - convert to timestamp for comparison
                $db_timestamp = strtotime($db_period_start);
            } else {
                $db_period_start = null;
            }
            
            // CRITICAL: Use timestamp comparison to avoid timezone/format issues
            // Only reset if timestamps differ by more than 1 hour (to account for minor differences)
            // This prevents false resets due to timezone or format mismatches
            $period_changed = false;
            if ($db_timestamp && $stripe_timestamp) {
                $time_diff = abs($stripe_timestamp - $db_timestamp);
                $period_changed = ($time_diff > 3600); // More than 1 hour difference = new period
                
                // Log the comparison for debugging
                error_log("Subscription period check for user {$user_id}: DB timestamp={$db_timestamp} (" . ($db_period_start ?: 'N/A') . "), Stripe timestamp={$stripe_timestamp} (" . $stripe_period_start . "), Time diff={$time_diff}s, Period changed=" . ($period_changed ? 'YES' : 'NO'));
                
                // If period appears to have changed, check current usage before resetting
                if ($period_changed) {
                    $current_usage_stmt = $pdo->prepare("
                        SELECT tracks_created, track_limit, subscription_period_start 
                        FROM monthly_track_usage
                        WHERE user_id = ? AND subscription_id = ?
                        ORDER BY subscription_period_start DESC
                        LIMIT 1
                    ");
                    $current_usage_stmt->execute([$user_id, $subscription_db_id]);
                    $current_usage = $current_usage_stmt->fetch(PDO::FETCH_ASSOC);
                    
                    if ($current_usage && $current_usage['tracks_created'] > 0) {
                        error_log("WARNING: About to reset usage with {$current_usage['tracks_created']}/{$current_usage['track_limit']} tracks created. Old period: {$db_period_start}, New period: {$stripe_period_start}. This is expected on billing renewal.");
                    }
                }
            } else {
                // If we can't compare, log warning but don't reset
                error_log("WARNING: Cannot compare subscription periods for user {$user_id}. DB timestamp: " . ($db_timestamp ?: 'NULL') . ", Stripe timestamp: " . ($stripe_timestamp ?: 'NULL') . ". NOT resetting usage.");
            }
            
            // Get track limit from config (use new plan name if changed)
            $plans_config = require __DIR__ . '/../config/subscription_plans.php';
            $plan_name_to_use = $plan_changed ? $new_plan_name : $old_plan_name;
            $track_limit = 5; // Default
            if (isset($plans_config[$plan_name_to_use]) && isset($plans_config[$plan_name_to_use]['tracks_per_month'])) {
                $track_limit = $plans_config[$plan_name_to_use]['tracks_per_month'];
            }
            
            if ($period_changed) {
                // New billing period started - reset usage for this period
                $new_period_start = $stripe_period_start;
                $year_month = date('Y-m', $subscription->current_period_start); // For backward compatibility
                
                // SAFEGUARD: Check if usage record already exists for this period
                $check_existing = $pdo->prepare("
                    SELECT id, tracks_created, track_limit 
                    FROM monthly_track_usage
                    WHERE user_id = ? AND subscription_period_start = ?
                ");
                $check_existing->execute([$user_id, $new_period_start]);
                $existing_usage = $check_existing->fetch(PDO::FETCH_ASSOC);
                
                if ($existing_usage) {
                    // Record already exists - only update if tracks_created is 0 (shouldn't happen, but be safe)
                    if ($existing_usage['tracks_created'] > 0) {
                        error_log("WARNING: Usage record already exists for period {$new_period_start} with {$existing_usage['tracks_created']} tracks. NOT resetting to prevent data loss.");
                    } else {
                        // Update existing record (shouldn't happen, but handle gracefully)
                        $stmt = $pdo->prepare("
                            UPDATE monthly_track_usage
                            SET track_limit = ?,
                                reset_at = NOW(),
                                updated_at = NOW()
                            WHERE user_id = ? AND subscription_period_start = ?
                        ");
                        $stmt->execute([$track_limit, $user_id, $new_period_start]);
                        error_log("Updated existing usage record for user {$user_id}, period: {$new_period_start}, limit: {$track_limit}");
                    }
                } else {
                    // Create new usage record for new subscription period (resets to 0)
                    $stmt = $pdo->prepare("
                        INSERT INTO monthly_track_usage (
                            user_id, subscription_id, subscription_period_start, 
                            year_month, tracks_created, track_limit, reset_at
                        )
                        VALUES (?, ?, ?, ?, 0, ?, NOW())
                    ");
                    $stmt->execute([
                        $user_id, 
                        $subscription_db_id, 
                        $new_period_start, 
                        $year_month, 
                        $track_limit
                    ]);
                    
                    // Log the reset
                    error_log("Subscription renewed - Created new usage record for user {$user_id}, new period: {$new_period_start}, limit: {$track_limit}, plan: {$plan_name_to_use}");
                }
            } elseif ($plan_changed) {
                // Plan changed but same period - update track limit for current period
                $current_period_start = $stripe_period_start;
                $year_month = date('Y-m', $subscription->current_period_start);
                
                // Get current tracks_created to check if we need to cap it
                $current_usage_stmt = $pdo->prepare("
                    SELECT tracks_created, track_limit FROM monthly_track_usage
                    WHERE user_id = ? AND subscription_period_start = ?
                ");
                $current_usage_stmt->execute([$user_id, $current_period_start]);
                $current_usage = $current_usage_stmt->fetch(PDO::FETCH_ASSOC);
                
                $current_tracks_created = (int)($current_usage['tracks_created'] ?? 0);
                $old_track_limit = (int)($current_usage['track_limit'] ?? 0);
                $is_downgrade = $track_limit < $old_track_limit;
                
                // CRITICAL: If downgrading and tracks_created exceeds new limit, cap it
                $new_tracks_created = $current_tracks_created;
                if ($is_downgrade && $current_tracks_created > $track_limit) {
                    $new_tracks_created = $track_limit;
                    error_log("SECURITY: User {$user_id} downgraded from {$old_plan_name} ({$old_track_limit} tracks) to {$new_plan_name} ({$track_limit} tracks). Capping tracks_created from {$current_tracks_created} to {$track_limit}");
                }
                
                // Update existing usage record with new limit (and capped tracks_created if downgrading)
                $stmt = $pdo->prepare("
                    UPDATE monthly_track_usage
                    SET track_limit = ?,
                        tracks_created = ?,
                        updated_at = NOW()
                    WHERE user_id = ? AND subscription_period_start = ?
                ");
                $stmt->execute([
                    $track_limit,
                    $new_tracks_created,
                    $user_id,
                    $current_period_start
                ]);
                
                $action = $is_downgrade ? 'Downgraded' : 'Upgraded';
                error_log("Plan {$action} - Updated track limit for user {$user_id}, plan: {$old_plan_name} → {$new_plan_name}, limit: {$old_track_limit} → {$track_limit}, tracks: {$current_tracks_created} → {$new_tracks_created}");
                
                // ============================================================
                // GRACE PERIOD CONVERSION (on upgrade only)
                // If user upgrades from free within 30 days, convert eligible tracks
                // ============================================================
                if (!$is_downgrade && $old_plan_name === 'free' && $new_plan_name !== 'free') {
                    try {
                        $grace_conversion_result = convertGracePeriodTracks($user_id);
                        if ($grace_conversion_result['success'] && $grace_conversion_result['converted_count'] > 0) {
                            error_log("Grace period conversion on plan change: User $user_id converted {$grace_conversion_result['converted_count']} tracks to commercial rights");
                        }
                    } catch (Exception $grace_e) {
                        error_log("Grace period conversion warning (non-fatal): " . $grace_e->getMessage());
                    }
                }
            }
        }
        
        // Log update
        $action_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'handleSubscriptionUpdated',
            'subscription_id' => $subscription->id,
            'status' => $subscription->status
        ];
        
        $action_log_file = __DIR__ . '/../logs/stripe_actions.log';
        file_put_contents($action_log_file, json_encode($action_log) . "\n", FILE_APPEND | LOCK_EX);
        
    } catch (Exception $e) {
        $error_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'handleSubscriptionUpdated_error',
            'subscription_id' => $subscription->id,
            'error' => $e->getMessage()
        ];
        
        $error_log_file = __DIR__ . '/../logs/stripe_actions.log';
        file_put_contents($error_log_file, json_encode($error_log) . "\n", FILE_APPEND | LOCK_EX);
    }
}

function handleSubscriptionDeleted($subscription) {
    require_once __DIR__ . '/../config/database.php';
    
    try {
        $pdo = getDBConnection();
        
        // Find user by Stripe customer ID
        $customer_id = $subscription->customer;
        $stmt = $pdo->prepare("SELECT id FROM users WHERE stripe_customer_id = ?");
        $stmt->execute([$customer_id]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if ($user) {
            $user_id = $user['id'];
            
            // Update subscription status
            $stmt = $pdo->prepare("
                UPDATE user_subscriptions
                SET status = 'canceled',
                    updated_at = NOW()
                WHERE stripe_subscription_id = ?
            ");
            $stmt->execute([$subscription->id]);
            
            // Update user plan to 'free' when subscription is cancelled
            $stmt = $pdo->prepare("
                UPDATE users 
                SET plan = 'free'
                WHERE id = ?
            ");
            $stmt->execute([$user_id]);
            
            // Log the action
            $action_log = [
                'timestamp' => date('Y-m-d H:i:s'),
                'action' => 'handleSubscriptionDeleted',
                'subscription_id' => $subscription->id,
                'customer_id' => $customer_id,
                'user_id' => $user_id,
                'status' => $subscription->status,
                'plan_downgraded_to' => 'free'
            ];
            
            $action_log_file = __DIR__ . '/../logs/stripe_actions.log';
            file_put_contents($action_log_file, json_encode($action_log) . "\n", FILE_APPEND | LOCK_EX);
        } else {
            // Log if user not found
            $action_log = [
                'timestamp' => date('Y-m-d H:i:s'),
                'action' => 'handleSubscriptionDeleted_user_not_found',
                'subscription_id' => $subscription->id,
                'customer_id' => $customer_id,
                'status' => $subscription->status
            ];
            
            $action_log_file = __DIR__ . '/../logs/stripe_actions.log';
            file_put_contents($action_log_file, json_encode($action_log) . "\n", FILE_APPEND | LOCK_EX);
        }
    } catch (Exception $e) {
        // Log error
        $error_log = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'handleSubscriptionDeleted_error',
            'subscription_id' => $subscription->id,
            'customer_id' => $subscription->customer,
            'error' => $e->getMessage()
        ];
        
        $error_log_file = __DIR__ . '/../logs/stripe_actions.log';
        file_put_contents($error_log_file, json_encode($error_log) . "\n", FILE_APPEND | LOCK_EX);
    }
}

function handleInvoicePaymentSucceeded($invoice) {
    // Handle successful invoice payment
    $action_log = [
        'timestamp' => date('Y-m-d H:i:s'),
        'action' => 'handleInvoicePaymentSucceeded',
        'invoice_id' => $invoice->id,
        'subscription_id' => $invoice->subscription,
        'amount_paid' => $invoice->amount_paid / 100
    ];
    
    $action_log_file = __DIR__ . '/../logs/stripe_actions.log';
    file_put_contents($action_log_file, json_encode($action_log) . "\n", FILE_APPEND | LOCK_EX);
}

function handleInvoicePaymentFailed($invoice) {
    // Handle failed invoice payment
    $action_log = [
        'timestamp' => date('Y-m-d H:i:s'),
        'action' => 'handleInvoicePaymentFailed',
        'invoice_id' => $invoice->id,
        'subscription_id' => $invoice->subscription,
        'attempt_count' => $invoice->attempt_count
    ];
    
    $action_log_file = __DIR__ . '/../logs/stripe_actions.log';
    file_put_contents($action_log_file, json_encode($action_log) . "\n", FILE_APPEND | LOCK_EX);
}

// Process event ticket purchase
function processEventTicketPurchase($user_id, $event_id, $paymentIntent, $quantity = 1) {
    require_once __DIR__ . '/../config/database.php';
    
    try {
        $pdo = getDBConnection();
        $pdo->beginTransaction();
        
        // Get event info
        $stmt = $pdo->prepare("SELECT id, creator_id, ticket_price FROM events WHERE id = ?");
        $stmt->execute([$event_id]);
        $event = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$event) {
            throw new Exception("Event not found");
        }
        
        // Get quantity from parameter or metadata (default to 1 if not set)
        if ($quantity <= 0) {
            $quantity = isset($paymentIntent->metadata->quantity) ? max(1, (int)$paymentIntent->metadata->quantity) : 1;
        }
        $unit_price = $event['ticket_price'];
        $total_price = $unit_price * $quantity;
        
        $ticket_ids = [];
        $ticket_codes = [];
        
        // Create multiple tickets
        for ($i = 0; $i < $quantity; $i++) {
            // Generate unique ticket code
            $ticket_code = 'EVT-' . strtoupper(substr(md5($event_id . $user_id . time() . $i), 0, 12));
            
            // Generate QR code data
            $qr_data = json_encode([
                'ticket_code' => $ticket_code,
                'event_id' => $event_id,
                'user_id' => $user_id,
                'timestamp' => time()
            ]);
            
            // Create ticket
            $stmt = $pdo->prepare("
                INSERT INTO event_tickets (
                    event_id, user_id, ticket_code, qr_code_data, 
                    price_paid, payment_method, stripe_payment_intent_id, status
                ) VALUES (?, ?, ?, ?, ?, 'stripe', ?, 'confirmed')
            ");
            $stmt->execute([
                $event_id,
                $user_id,
                $ticket_code,
                $qr_data,
                $unit_price,
                $paymentIntent->id
            ]);
            
            $ticket_id = $pdo->lastInsertId();
            $ticket_ids[] = $ticket_id;
            $ticket_codes[] = $ticket_code;
            
            // Record sale for each ticket
            $stmt = $pdo->prepare("
                INSERT INTO event_ticket_sales (
                    ticket_id, event_id, buyer_id, event_creator_id, 
                    amount, revenue_recipient, recipient_id
                ) VALUES (?, ?, ?, ?, ?, 'event_creator', ?)
            ");
            $stmt->execute([
                $ticket_id,
                $event_id,
                $user_id,
                $event['creator_id'],
                $unit_price,
                $event['creator_id']
            ]);
            $sale_id = $pdo->lastInsertId();
            
            // Notify organizer of the ticket sale (only once per purchase, not per ticket)
            if ($i === 0) {
                require_once __DIR__ . '/../utils/artist_notifications.php';
                notifyOrganizerOfTicketSale(
                    $event['creator_id'],
                    $event_id,
                    $user_id,
                    $unit_price,
                    $quantity,
                    $sale_id
                );
            }
        }
        
        // Update event attendee count
        $stmt = $pdo->prepare("
            INSERT INTO event_attendees (event_id, user_id, status)
            VALUES (?, ?, 'attending')
            ON DUPLICATE KEY UPDATE status = 'attending'
        ");
        $stmt->execute([$event_id, $user_id]);
        
        $pdo->commit();
        
        // Log success
        $log_data = [
            'timestamp' => date('Y-m-d H:i:s'),
            'action' => 'event_ticket_purchased',
            'ticket_ids' => $ticket_ids,
            'ticket_codes' => $ticket_codes,
            'quantity' => $quantity,
            'total_price' => $total_price,
            'event_id' => $event_id,
            'user_id' => $user_id,
            'payment_intent_id' => $paymentIntent->id
        ];
        
        $log_file = __DIR__ . '/../logs/event_tickets.log';
        file_put_contents($log_file, json_encode($log_data) . "\n", FILE_APPEND | LOCK_EX);
        
    } catch (Exception $e) {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }
        error_log("Event Ticket Purchase Error: " . $e->getMessage());
    }
}
?> 

CasperSecurity Mini