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/private_html/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/gositeme/domains/soundstudiopro.com/private_html/STEPHANE_PLAN_SYNC_ISSUE_ANALYSIS.md
# Stephane Plan Sync Issue - Root Cause Analysis & Permanent Fix

**Date:** 2025-12-19  
**User:** Stephane (ID: 5, Email: stevenberg450@gmail.com)  
**Issue:** Subscription exists in Stripe but not properly synced to database

---

## What Happened

Stephane paid and upgraded to Essential plan ($5/month), but:
- ✅ Subscription exists in Stripe (3 subscriptions found)
- ❌ Plan not showing correctly in database (`users.plan` was 'free')
- ❌ `user_subscriptions` table had incorrect or missing records
- ❌ User saw "free" plan on various pages despite having active subscription

**Temporary Fix Applied:**
- Created `sync_subscription_from_stripe.php` script
- Manually synced subscription from Stripe to database
- Updated `users.plan` to 'essential'
- Created/updated `user_subscriptions` record

---

## Root Causes

### 1. **Webhook Failure Scenarios**

The subscription recording relies on two mechanisms:

**A. Primary: `subscription_success.php` (Immediate)**
- Runs when user completes Stripe checkout and is redirected
- **Failure Points:**
  - User closes browser before redirect completes
  - Network timeout during redirect
  - User uses back button
  - JavaScript error prevents redirect

**B. Backup: `webhooks/stripe.php` (Delayed)**
- Runs when Stripe sends `customer.subscription.created` event
- **Failure Points:**
  - Webhook endpoint not configured in Stripe
  - Webhook signature verification fails
  - Network issues preventing webhook delivery
  - Webhook processing error (exception thrown)
  - User lookup fails (customer ID mismatch)

### 2. **User Identification Issues**

In `handleSubscriptionCreated()` (webhooks/stripe.php:1265-1281):
```php
// 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}");
    }
}
```

**Failure Scenarios:**
- `stripe_customer_id` not set in `users` table when subscription created
- Customer ID mismatch (user has multiple Stripe customers)
- Metadata missing `user_id` (shouldn't happen, but possible)

### 3. **Plan Detection Issues**

Plan detection has multiple fallbacks (webhooks/stripe.php:1283-1312):
1. Match price ID to config (most reliable)
2. Check price metadata
3. Check subscription metadata
4. Default to 'essential'

**Failure Scenarios:**
- Price ID not in `config/subscription_plans.php`
- Price metadata missing
- Subscription metadata missing
- All fallbacks fail → defaults to 'essential' (might be wrong plan)

### 4. **Database Transaction Failures**

If database transaction fails (webhooks/stripe.php:1362-1431):
- Exception thrown → transaction rolled back
- Subscription not recorded
- Error logged but webhook returns 200 (Stripe won't retry)
- **No automatic retry mechanism**

### 5. **Multiple Subscriptions**

Stephane had **3 subscriptions** in Stripe:
- `sub_1SbU0PD0zXLMB4gHAhMLA8bt` (active)
- `sub_1SbTqRD0zXLMB4gHIzHNGJlt` (active)
- `sub_1SX3lTD0zXLMB4gHw7Sh8piJ` (active, was in DB as 'free')

**Root Cause:** User created multiple subscriptions (possibly due to clicking "Subscribe" multiple times, or network issues causing duplicate submissions).

**Current Fix:** Webhook now cancels old subscriptions (lines 1318-1429), but this only works if webhook fires successfully.

---

## Permanent Fix Strategy

### 1. **Automatic Sync on Login** ✅ RECOMMENDED

Add subscription sync check when user logs in:

**File:** `auth/login.php` or `includes/header.php`

```php
// After successful login, check if subscription needs syncing
if (isset($_SESSION['user_id'])) {
    require_once __DIR__ . '/utils/subscription_helpers.php';
    
    $user = getUserById($_SESSION['user_id']);
    if ($user && !empty($user['stripe_customer_id'])) {
        // Check if user has active subscription in Stripe but not in DB
        $has_active_sub = hasActiveSubscription($_SESSION['user_id']);
        $effective_plan = getEffectivePlan($_SESSION['user_id']);
        
        // If user has Stripe customer ID but no active subscription in DB,
        // trigger background sync (don't block login)
        if (!$has_active_sub && $effective_plan === 'free' && !empty($user['stripe_customer_id'])) {
            // Queue sync job (or run async)
            // This ensures subscription is synced within minutes of login
        }
    }
}
```

### 2. **Periodic Sync Cron Job** ✅ RECOMMENDED

Create daily cron job to sync all subscriptions:

**File:** `cron/sync_subscriptions_from_stripe.php`

```php
<?php
/**
 * Daily Subscription Sync from Stripe
 * Syncs all active subscriptions from Stripe to database
 * Runs daily to catch any missed webhooks
 */

require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../utils/subscription_helpers.php';

$stripe_secret = 'sk_live_...';
$pdo = getDBConnection();

// Get all users with Stripe customer IDs
$stmt = $pdo->query("SELECT id, email, stripe_customer_id FROM users WHERE stripe_customer_id IS NOT NULL AND stripe_customer_id != ''");
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);

foreach ($users as $user) {
    try {
        // Fetch subscriptions from Stripe
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, "https://api.stripe.com/v1/subscriptions?customer=" . urlencode($user['stripe_customer_id']) . "&limit=10&status=all");
        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) {
            $subscriptions_data = json_decode($response, true);
            $subscriptions = $subscriptions_data['data'] ?? [];
            
            // Sync each active subscription
            foreach ($subscriptions as $stripe_sub) {
                if (in_array($stripe_sub['status'], ['active', 'trialing'])) {
                    // Use same logic as webhook to sync
                    syncSubscriptionFromStripe($stripe_sub, $user['id']);
                }
            }
        }
    } catch (Exception $e) {
        error_log("Cron sync error for user {$user['id']}: " . $e->getMessage());
    }
}
```

**Cron Setup:**
```bash
# Run daily at 2 AM
0 2 * * * /usr/bin/php /home/gositeme/domains/soundstudiopro.com/public_html/cron/sync_subscriptions_from_stripe.php
```

### 3. **Improved Webhook Error Handling** ✅ RECOMMENDED

**File:** `webhooks/stripe.php`

**Current Issue:** If webhook fails, it returns 200 (success) to Stripe, so Stripe won't retry.

**Fix:** Return appropriate HTTP codes and add retry logic:

```php
try {
    handleSubscriptionCreated($subscription);
    http_response_code(200);
    echo json_encode(['status' => 'success']);
} catch (Exception $e) {
    error_log("Webhook error: " . $e->getMessage());
    
    // Return 500 to trigger Stripe retry (for transient errors)
    // Return 400 for permanent errors (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']);
    }
}
```

### 4. **Self-Healing in getEffectivePlan()** ✅ RECOMMENDED

**File:** `utils/subscription_helpers.php`

Add automatic sync trigger when discrepancy detected:

```php
function getEffectivePlan($user_id) {
    // ... existing code ...
    
    // If we detect a discrepancy (user has Stripe customer ID but no active subscription),
    // trigger background sync
    $user = getUserById($user_id);
    if ($user && !empty($user['stripe_customer_id'])) {
        $active_sub = hasActiveSubscription($user_id);
        if (!$active_sub) {
            // Queue sync job (don't block current request)
            // This will sync in background
            queueSubscriptionSync($user_id, $user['stripe_customer_id']);
        }
    }
    
    // ... rest of function ...
}
```

### 5. **Webhook Retry Queue** ⚠️ ADVANCED

For critical reliability, implement a retry queue:

**File:** `webhooks/stripe_retry_queue.php`

- Store failed webhook events in database
- Retry with exponential backoff
- Alert admin after N failed retries

---

## Implementation Priority

### 🔴 **High Priority (Do First)**

1. **Periodic Sync Cron Job** - Catches all missed webhooks daily
2. **Improved Webhook Error Handling** - Ensures Stripe retries on failures
3. **Automatic Sync on Login** - Fixes issues immediately when user logs in

### 🟡 **Medium Priority**

4. **Self-Healing in getEffectivePlan()** - Proactive sync when discrepancy detected
5. **Better Logging & Monitoring** - Track sync failures and alert admin

### 🟢 **Low Priority (Nice to Have)**

6. **Webhook Retry Queue** - Advanced reliability (only if issues persist)

---

## Testing the Fix

After implementing permanent fixes:

1. **Test Webhook Failure:**
   - Temporarily break webhook endpoint
   - Create subscription
   - Verify cron job syncs it within 24 hours

2. **Test User Login Sync:**
   - Create subscription with webhook disabled
   - Log in as user
   - Verify subscription synced immediately

3. **Test Multiple Subscriptions:**
   - Create multiple subscriptions for same user
   - Verify only one active, others canceled

---

## Monitoring

Add monitoring to track:
- Number of subscriptions synced per day (cron job)
- Webhook failure rate
- Users with Stripe customer ID but no active subscription
- Plan detection failures (price ID not found)

---

## Summary

**Root Cause:** Webhook failures + no automatic recovery mechanism

**Permanent Fix:** Multi-layered approach:
1. Daily cron sync (catches all missed webhooks)
2. Login-time sync (immediate fix for users)
3. Better webhook error handling (ensures retries)
4. Self-healing detection (proactive sync)

This ensures subscriptions are **always** synced, even if webhooks fail.

CasperSecurity Mini