$provider, 'headers' => $request->headers->all(), ]); $result = $this->orchestrator->processWebhook($provider, $request); // Process Phase 4 specific events $this->processPhase4Events($provider, $result); // Send notification for successful payments if ($this->isSuccessfulPayment($result)) { $this->sendPaymentNotification($provider, $result); } // Send notifications for Phase 4 events $this->sendPhase4Notifications($provider, $result); return response('OK', 200); } catch (Exception $e) { Log::error("{$provider} webhook processing error", [ 'provider' => $provider, 'error' => $e->getMessage(), 'request_data' => $request->getContent(), ]); $this->sendErrorNotification($provider, $e); return response('Processing error', 400); } } /** * Legacy Oxapay webhook handler (for backward compatibility) */ public function oxapay(Request $request): ResponseFactory|Response { return $this->handle($request, 'oxapay'); } /** * Stripe webhook handler */ public function stripe(Request $request): ResponseFactory|Response { return $this->handle($request, 'stripe'); } /** * Lemon Squeezy webhook handler */ public function lemonSqueezy(Request $request): ResponseFactory|Response { return $this->handle($request, 'lemon_squeezy'); } /** * Polar webhook handler */ public function polar(Request $request): ResponseFactory|Response { return $this->handle($request, 'polar'); } /** * Crypto webhook handler */ public function crypto(Request $request): ResponseFactory|Response { return $this->handle($request, 'crypto'); } /** * Check if webhook result indicates a successful payment */ protected function isSuccessfulPayment(array $result): bool { if (! ($result['success'] ?? false)) { return false; } $eventType = $result['event_type'] ?? ''; $status = $result['status'] ?? ''; // Check for successful payment events $successfulEvents = [ 'payment.succeeded', 'invoice.payment_succeeded', 'checkout.session.completed', 'subscription.created', 'subscription.updated', 'customer.subscription.created', 'charge.succeeded', 'payment_intent.succeeded', 'invoicepaid', 'Paid', // OxaPay status ]; return in_array($eventType, $successfulEvents) || in_array($status, ['paid', 'succeeded', 'completed', 'active']); } /** * Send notification for successful payment */ protected function sendPaymentNotification(string $provider, array $result): void { $eventType = $result['event_type'] ?? $result['status'] ?? 'unknown'; $subscriptionId = $result['subscription_id'] ?? null; $amount = $result['amount'] ?? 'Unknown'; $currency = $result['currency'] ?? 'Unknown'; $email = $result['email'] ?? 'Unknown'; $message = "✅ {$this->getProviderDisplayName($provider)} Payment Success\n". "Event: {$eventType}\n". "Amount: {$amount} {$currency}\n". ($subscriptionId ? "Subscription ID: {$subscriptionId}\n" : ''). ($email !== 'Unknown' ? "Email: {$email}\n" : ''). 'Time: '.now()->toDateTimeString(); $this->sendTelegramNotification($message); } /** * Send error notification */ protected function sendErrorNotification(string $provider, Exception $e): void { $message = "❌ {$this->getProviderDisplayName($provider)} Webhook Error\n". "Error: {$e->getMessage()}\n". 'Time: '.now()->toDateTimeString(); $this->sendTelegramNotification($message); } /** * Process Phase 4 specific events */ protected function processPhase4Events(string $provider, array $result): void { $eventType = $result['event_type'] ?? ''; $subscriptionId = $result['subscription_id'] ?? null; if (! $subscriptionId) { return; } $subscription = Subscription::find($subscriptionId); if (! $subscription) { Log::warning('Subscription not found for Phase 4 processing', [ 'subscription_id' => $subscriptionId, 'provider' => $provider, ]); return; } // Handle coupon usage events if ($this->isCouponUsageEvent($result)) { $this->processCouponUsage($subscription, $result); } // Handle trial events if ($this->isTrialEvent($result)) { $this->processTrialEvent($subscription, $result); } // Handle subscription change events if ($this->isSubscriptionChangeEvent($result)) { $this->processSubscriptionChangeEvent($subscription, $result); } // Handle migration events if ($this->isMigrationEvent($result)) { $this->processMigrationEvent($subscription, $result); } } /** * Process coupon usage */ protected function processCouponUsage(Subscription $subscription, array $result): void { try { $couponCode = $result['coupon_code'] ?? null; $discountAmount = $result['discount_amount'] ?? 0; if (! $couponCode) { return; } $coupon = Coupon::where('code', $couponCode)->first(); if (! $coupon) { Log::warning('Coupon not found', ['coupon_code' => $couponCode]); return; } // Apply coupon to subscription if not already applied $existingUsage = $subscription->couponUsages() ->where('coupon_id', $coupon->id) ->first(); if (! $existingUsage) { $subscription->applyCoupon($coupon, $discountAmount); Log::info('Coupon applied via webhook', [ 'subscription_id' => $subscription->id, 'coupon_id' => $coupon->id, 'discount_amount' => $discountAmount, ]); } } catch (Exception $e) { Log::error('Failed to process coupon usage', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); } } /** * Process trial events */ protected function processTrialEvent(Subscription $subscription, array $result): void { try { $eventType = $result['event_type'] ?? ''; $newTrialEnd = $result['trial_ends_at'] ?? null; if (! $newTrialEnd) { return; } switch ($eventType) { case 'trial.will_end': case 'trial.ending': // Send reminder notification $this->sendTrialEndingNotification($subscription); break; case 'trial.extended': // Record trial extension $daysExtended = $result['trial_extension_days'] ?? 7; $reason = $result['extension_reason'] ?? 'Extended by provider'; $subscription->extendTrial($daysExtended, $reason, 'automatic'); break; case 'trial.ended': // Record trial completion $this->recordTrialCompletion($subscription, $result); break; } } catch (Exception $e) { Log::error('Failed to process trial event', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); } } /** * Process subscription change events */ protected function processSubscriptionChangeEvent(Subscription $subscription, array $result): void { try { $eventType = $result['event_type'] ?? ''; $oldPlanId = $result['old_plan_id'] ?? null; $newPlanId = $result['new_plan_id'] ?? null; switch ($eventType) { case 'plan.changed': case 'subscription.updated': if ($oldPlanId && $newPlanId) { $changeType = $this->determinePlanChangeType($oldPlanId, $newPlanId); $this->orchestrator->recordSubscriptionChange( $subscription, $changeType, "Plan changed from {$oldPlanId} to {$newPlanId}", ['plan_id' => $oldPlanId], ['plan_id' => $newPlanId], 'Plan change via webhook' ); } break; case 'subscription.paused': $this->orchestrator->recordSubscriptionChange( $subscription, 'pause', 'Subscription paused via webhook', null, ['status' => 'paused'], 'Paused by provider' ); break; case 'subscription.resumed': $this->orchestrator->recordSubscriptionChange( $subscription, 'resume', 'Subscription resumed via webhook', ['status' => 'paused'], ['status' => 'active'], 'Resumed by provider' ); break; case 'subscription.cancelled': $reason = $result['cancellation_reason'] ?? 'Cancelled by provider'; $this->orchestrator->recordSubscriptionChange( $subscription, 'cancel', 'Subscription cancelled via webhook', null, ['status' => 'cancelled', 'reason' => $reason], $reason ); break; } } catch (Exception $e) { Log::error('Failed to process subscription change event', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); } } /** * Process migration events */ protected function processMigrationEvent(Subscription $subscription, array $result): void { try { $targetProvider = $result['target_provider'] ?? null; $migrationBatchId = $result['migration_batch_id'] ?? null; if (! $targetProvider || ! $migrationBatchId) { return; } $subscription->update([ 'migration_batch_id' => $migrationBatchId, 'is_migrated' => true, 'legacy_data' => array_merge($subscription->legacy_data ?? [], [ 'migration_source' => $result['source_provider'] ?? $subscription->provider, 'migration_date' => now()->toDateTimeString(), 'migration_reason' => $result['migration_reason'] ?? 'Provider migration', ]), ]); Log::info('Subscription migration recorded', [ 'subscription_id' => $subscription->id, 'migration_batch_id' => $migrationBatchId, 'target_provider' => $targetProvider, ]); } catch (Exception $e) { Log::error('Failed to process migration event', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); } } /** * Send Phase 4 specific notifications */ protected function sendPhase4Notifications(string $provider, array $result): void { $eventType = $result['event_type'] ?? ''; switch ($eventType) { case 'coupon.applied': $this->sendCouponAppliedNotification($provider, $result); break; case 'trial.ending': $this->sendTrialEndingNotification($result); break; case 'trial.extended': $this->sendTrialExtendedNotification($provider, $result); break; case 'plan.changed': $this->sendPlanChangedNotification($provider, $result); break; case 'subscription.migrated': $this->sendMigrationNotification($provider, $result); break; } } /** * Send coupon applied notification */ protected function sendCouponAppliedNotification(string $provider, array $result): void { $couponCode = $result['coupon_code'] ?? 'Unknown'; $discountAmount = $result['discount_amount'] ?? 0; $email = $result['email'] ?? 'Unknown'; $message = "🎫 Coupon Applied\n". "Provider: {$this->getProviderDisplayName($provider)}\n". "Coupon: {$couponCode}\n". "Discount: {$discountAmount}\n". "Email: {$email}\n". 'Time: '.now()->toDateTimeString(); $this->sendTelegramNotification($message); } /** * Send trial ending notification */ protected function sendTrialEndingNotification($subscriptionOrResult): void { if ($subscriptionOrResult instanceof Subscription) { $subscription = $subscriptionOrResult; $email = $subscription->user?->email ?? 'Unknown'; $trialEndsAt = $subscription->trial_ends_at?->toDateTimeString() ?? 'Unknown'; } else { $email = $subscriptionOrResult['email'] ?? 'Unknown'; $trialEndsAt = $subscriptionOrResult['trial_ends_at'] ?? 'Unknown'; } $message = "⏰ Trial Ending Soon\n". "Email: {$email}\n". "Trial ends: {$trialEndsAt}\n". 'Time: '.now()->toDateTimeString(); $this->sendTelegramNotification($message); } /** * Send trial extended notification */ protected function sendTrialExtendedNotification(string $provider, array $result): void { $daysExtended = $result['trial_extension_days'] ?? 0; $newTrialEnd = $result['new_trial_ends_at'] ?? 'Unknown'; $reason = $result['extension_reason'] ?? 'Extended'; $message = "✅ Trial Extended\n". "Provider: {$this->getProviderDisplayName($provider)}\n". "Days extended: {$daysExtended}\n". "New trial end: {$newTrialEnd}\n". "Reason: {$reason}\n". 'Time: '.now()->toDateTimeString(); $this->sendTelegramNotification($message); } /** * Send plan changed notification */ protected function sendPlanChangedNotification(string $provider, array $result): void { $oldPlan = $result['old_plan_name'] ?? 'Unknown'; $newPlan = $result['new_plan_name'] ?? 'Unknown'; $email = $result['email'] ?? 'Unknown'; $message = "🔄 Plan Changed\n". "Provider: {$this->getProviderDisplayName($provider)}\n". "Email: {$email}\n". "Old plan: {$oldPlan}\n". "New plan: {$newPlan}\n". 'Time: '.now()->toDateTimeString(); $this->sendTelegramNotification($message); } /** * Send migration notification */ protected function sendMigrationNotification(string $provider, array $result): void { $sourceProvider = $result['source_provider'] ?? 'Unknown'; $targetProvider = $result['target_provider'] ?? 'Unknown'; $migrationBatchId = $result['migration_batch_id'] ?? 'Unknown'; $message = "🔄 Subscription Migration\n". "Source: {$this->getProviderDisplayName($sourceProvider)}\n". "Target: {$this->getProviderDisplayName($targetProvider)}\n". "Batch ID: {$migrationBatchId}\n". 'Time: '.now()->toDateTimeString(); $this->sendTelegramNotification($message); } /** * Record trial completion */ protected function recordTrialCompletion(Subscription $subscription, array $result): void { $this->orchestrator->recordSubscriptionChange( $subscription, 'trial_completed', 'Trial period completed', ['status' => 'trialing'], ['status' => $subscription->status], 'Trial ended naturally' ); } /** * Check if event is a coupon usage event */ protected function isCouponUsageEvent(array $result): bool { $eventType = $result['event_type'] ?? ''; return in_array($eventType, [ 'coupon.applied', 'discount.applied', 'coupon.redeemed', ]) || isset($result['coupon_code']); } /** * Check if event is a trial event */ protected function isTrialEvent(array $result): bool { $eventType = $result['event_type'] ?? ''; return in_array($eventType, [ 'trial.started', 'trial.will_end', 'trial.ending', 'trial.ended', 'trial.extended', ]) || isset($result['trial_ends_at']); } /** * Check if event is a subscription change event */ protected function isSubscriptionChangeEvent(array $result): bool { $eventType = $result['event_type'] ?? ''; return in_array($eventType, [ 'plan.changed', 'subscription.updated', 'subscription.paused', 'subscription.resumed', 'subscription.cancelled', ]) || isset($result['new_plan_id']); } /** * Check if event is a migration event */ protected function isMigrationEvent(array $result): bool { $eventType = $result['event_type'] ?? ''; return in_array($eventType, [ 'subscription.migrated', 'provider.migrated', ]) || isset($result['migration_batch_id']); } /** * Determine plan change type */ protected function determinePlanChangeType(?int $oldPlanId, ?int $newPlanId): string { if (! $oldPlanId || ! $newPlanId) { return 'plan_change'; } // This is a simplified determination - in practice you'd compare plan prices/features return $newPlanId > $oldPlanId ? 'plan_upgrade' : 'plan_downgrade'; } /** * Get display name for provider */ protected function getProviderDisplayName(string $provider): string { $displayNames = [ 'stripe' => 'Stripe', 'lemon_squeezy' => 'Lemon Squeezy', 'polar' => 'Polar.sh', 'oxapay' => 'OxaPay', 'crypto' => 'Crypto', 'activation_key' => 'Activation Key', ]; return $displayNames[$provider] ?? ucfirst($provider); } }