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/ROOT_CAUSE_FIX.md
# Root Cause Fix - Purchase Processing

## What Was Wrong (The Actual Problem)

The webhook handler had several critical issues:

1. **Silent Failures**: `processTrackPurchase()` caught exceptions, logged them, but returned `void`. The webhook had no way to know if processing succeeded or failed.

2. **No Retry Logic**: If a database transaction failed, the purchase was lost forever. No retry mechanism.

3. **No Idempotency**: If Stripe sent the webhook twice (which can happen), it would fail on the second attempt with "already purchased" error, but the first attempt might have failed silently.

4. **Always Returned 200 OK**: Even when processing failed, the webhook returned 200 OK to Stripe, so Stripe thought everything was fine and wouldn't retry.

## What I Fixed

### 1. Made Functions Return Success/Failure Status

**Before:**
```php
function processTrackPurchase(...) {
    try {
        // ... processing ...
    } catch (Exception $e) {
        // Just log and return void
        log_error($e);
    }
    // Returns nothing - caller has no idea if it worked
}
```

**After:**
```php
function processTrackPurchase(...) {
    try {
        // ... processing ...
        return ['success' => true, 'message' => '...', 'purchase_id' => $id];
    } catch (Exception $e) {
        return ['success' => false, 'message' => $e->getMessage(), 'purchase_id' => null];
    }
}
```

### 2. Added Idempotency Check

**Before:** Would fail if purchase already exists (webhook called twice)

**After:** Checks if purchase exists with same payment_intent_id, returns success if it does:
```php
// IDEMPOTENCY CHECK: If purchase already exists, return success
$stmt = $pdo->prepare("SELECT id FROM track_purchases 
    WHERE user_id = ? AND track_id = ? AND stripe_payment_intent_id = ?");
if ($existing_purchase) {
    return ['success' => true, 'message' => 'Already exists (idempotent)'];
}
```

### 3. Added Automatic Retry Logic

**New Function:** `processTrackPurchaseWithRetry()`
- Retries up to 3 times with exponential backoff (1s, 2s, 4s)
- Only retries on retryable errors (not "track not found" or "already purchased")
- Logs all retry attempts

### 4. Added Retry Queue for Persistent Failures

**New Function:** `schedulePurchaseRetry()`
- If all retries fail, schedules purchase for later processing
- Can be processed by a cron job
- Prevents permanent loss of purchases

### 5. Better Error Handling in Webhook Handler

**Before:**
```php
handleSuccessfulPayment($paymentIntent); // No error checking
```

**After:**
```php
try {
    $result = processTrackPurchaseWithRetry(...);
    if (!$result['success']) {
        schedulePurchaseRetry(...); // Schedule for later
    }
} catch (Exception $e) {
    // Log and schedule retry
    schedulePurchaseRetry(...);
}
```

## Result

Now the system:
- ✅ **Detects failures** - Functions return success/failure status
- ✅ **Retries automatically** - Up to 3 times with backoff
- ✅ **Handles duplicates** - Idempotent (safe to call multiple times)
- ✅ **Schedules retries** - For persistent failures
- ✅ **Logs everything** - Full audit trail

## The Cron Script is Still Useful

Even with these fixes, the cron script (`auto_fix_missing_purchases.php`) is still valuable as a **safety net** for:
- Edge cases we didn't anticipate
- Historical purchases that failed before the fix
- Webhook delivery failures (Stripe can't reach your server)
- Network issues between Stripe and your server

**Best Practice**: Use both:
1. **Root cause fix** (this) - Prevents most failures
2. **Cron safety net** - Catches anything that slips through

## Testing

To verify the fix works:

1. **Test idempotency**: Call webhook twice with same payment_intent_id - should succeed both times
2. **Test retry**: Temporarily break database connection, webhook should retry
3. **Monitor logs**: Check `logs/track_purchase_retries.log` for retry activity
4. **Monitor queue**: Check `logs/purchase_retry_queue.log` for scheduled retries

## Files Modified

- `webhooks/stripe.php` - Added retry logic, idempotency, return values

## Files Created (Safety Net)

- `auto_fix_missing_purchases.php` - Cron script for catching missed purchases
- `PURCHASE_FIX_EXPLANATION.md` - Explanation of the problem
- `AUTOMATION_SETUP.md` - Setup guide for cron script

CasperSecurity Mini