![]() 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/ |
# 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