diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index f638335..a2b039a 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -65,6 +65,8 @@ class Subscription extends Model 'amount', 'currency', 'expires_at', + 'uncanceled_at', + 'cancel_at_period_end', ]; protected $casts = [ diff --git a/app/NotifyMe.php b/app/NotifyMe.php index 51395a6..2d3f44e 100644 --- a/app/NotifyMe.php +++ b/app/NotifyMe.php @@ -2,13 +2,13 @@ namespace App; -use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Http; use Exception; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; trait NotifyMe { - public function sendTelegramNotification($message) + public function sendTelegramNotification($message): bool { $botToken = config('app.notify_tg_bot_token'); $chatId = config('app.notify_tg_chat_id'); @@ -28,9 +28,7 @@ trait NotifyMe ]; try { - $response = Http::post($url, $data); - - return $response->successful(); + return Http::post($url, $data)->successful(); } catch (Exception $e) { Log::error('Failed to send Telegram notification: '.$e->getMessage()); diff --git a/app/Services/Payments/Providers/ActivationKeyProvider.php b/app/Services/Payments/Providers/ActivationKeyProvider.php index 60b926a..ed5de0d 100644 --- a/app/Services/Payments/Providers/ActivationKeyProvider.php +++ b/app/Services/Payments/Providers/ActivationKeyProvider.php @@ -7,6 +7,7 @@ use App\Models\ActivationKey; use App\Models\Plan; use App\Models\Subscription; use App\Models\User; +use App\NotifyMe; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -14,6 +15,8 @@ use Illuminate\Support\Str; class ActivationKeyProvider implements PaymentProviderContract { + use NotifyMe; + protected array $config; public function __construct(array $config = []) @@ -74,6 +77,9 @@ class ActivationKeyProvider implements PaymentProviderContract DB::commit(); + // Notify activation key generated + $this->notifyActivationKeyGenerated($user, $plan, $activationKey); + return [ 'provider_subscription_id' => $keyRecord->id, 'status' => 'pending_activation', @@ -106,6 +112,9 @@ class ActivationKeyProvider implements PaymentProviderContract 'cancellation_reason' => $reason, ]); + // Notify subscription cancelled + $this->notifySubscriptionCancelled($subscription, $reason ?: 'Manual cancellation'); + return true; } catch (\Exception $e) { @@ -381,6 +390,10 @@ class ActivationKeyProvider implements PaymentProviderContract DB::commit(); + // Notify activation key redeemed and subscription activated + $this->notifyActivationKeyRedeemed($user, $plan, $activationKey); + $this->notifySubscriptionActivated($user, $plan, $subscription); + return [ 'success' => true, 'subscription_id' => $subscription->id, @@ -494,4 +507,79 @@ class ActivationKeyProvider implements PaymentProviderContract { throw new \Exception('Import to activation keys not implemented'); } + + // Notification methods + protected function notifyActivationKeyGenerated(User $user, Plan $plan, string $activationKey): void + { + $message = "πŸ”‘ ACTIVATION KEY GENERATED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + 'πŸ’° Price: $'.number_format($plan->price, 2)."\n". + "πŸͺ Provider: Activation Key\n". + "πŸ”‘ Key: {$activationKey}\n". + 'πŸ“… Generated: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifyActivationKeyRedeemed(User $user, Plan $plan, string $activationKey): void + { + $message = "βœ… ACTIVATION KEY REDEEMED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + "πŸͺ Provider: Activation Key\n". + "πŸ”‘ Key: {$activationKey}\n". + 'πŸ“… Redeemed: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifySubscriptionActivated(User $user, Plan $plan, Subscription $subscription): void + { + $message = "πŸŽ‰ SUBSCRIPTION ACTIVATED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + "πŸͺ Provider: Activation Key\n". + "πŸ”„ Subscription ID: {$subscription->id}\n". + 'πŸ“… Activated: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifyProviderError(string $operation, string $error, array $context = []): void + { + $contextStr = ''; + if (! empty($context)) { + $contextStr = 'πŸ“ Details: '.json_encode(array_slice($context, 0, 3, true), JSON_UNESCAPED_SLASHES)."\n"; + if (count($context) > 3) { + $contextStr .= 'πŸ“ Additional: '.(count($context) - 3).' more items'."\n"; + } + } + + $message = "🚨 PROVIDER ERROR\n". + "πŸͺ Provider: Activation Key\n". + "πŸ“‘ Operation: {$operation}\n". + "πŸ’₯ Error: {$error}\n". + $contextStr. + '⏰ Time: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifySubscriptionCancelled(Subscription $subscription, string $reason): void + { + $user = $subscription->user; + $plan = $subscription->plan; + + $message = "❌ SUBSCRIPTION CANCELLED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + "πŸͺ Provider: Activation Key\n". + "πŸ’­ Reason: {$reason}\n". + "πŸ†” Subscription ID: {$subscription->id}\n". + ($subscription->ends_at ? 'πŸ“… Effective: '.$subscription->ends_at->format('Y-m-d')."\n" : ''). + '⏰ Cancelled: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } } diff --git a/app/Services/Payments/Providers/OxapayProvider.php b/app/Services/Payments/Providers/OxapayProvider.php index ba622cd..8d6a9fd 100644 --- a/app/Services/Payments/Providers/OxapayProvider.php +++ b/app/Services/Payments/Providers/OxapayProvider.php @@ -7,6 +7,7 @@ use App\Models\PaymentProvider as PaymentProviderModel; use App\Models\Plan; use App\Models\Subscription; use App\Models\User; +use App\NotifyMe; use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; @@ -15,6 +16,8 @@ use Illuminate\Support\Facades\Log; class OxapayProvider implements PaymentProviderContract { + use NotifyMe; + protected array $config; protected bool $sandbox; @@ -52,7 +55,6 @@ class OxapayProvider implements PaymentProviderContract try { $providerModel = PaymentProviderModel::where('name', 'oxapay')->first(); - if ($providerModel && $providerModel->configuration) { return $providerModel->configuration; } @@ -163,6 +165,9 @@ class OxapayProvider implements PaymentProviderContract 'cancellation_reason' => $reason, ]); + // Notify subscription cancelled + $this->notifySubscriptionCancelled($subscription, $reason ?: 'Manual cancellation'); + return true; } catch (\Exception $e) { @@ -273,6 +278,13 @@ class OxapayProvider implements PaymentProviderContract 'last_provider_sync' => now(), ]); + // Notify payment created + $this->notifyPaymentCreated($user, $plan, [ + 'amount' => $paymentData['amount'] ?? $amount, + 'currency' => $paymentData['currency'] ?? $currency, + 'payment_id' => $paymentData['track_id'] ?? null, + ]); + Log::info('OxaPay subscription created', [ 'subscription_id' => $subscription->id, 'track_id' => $paymentData['track_id'], @@ -321,11 +333,7 @@ class OxapayProvider implements PaymentProviderContract { try { $payload = $request->getContent(); - $signature = $request->header('HMAC'); - - if (! $this->validateWebhook($request)) { - throw new \Exception('Invalid webhook signature'); - } + $signature = $request->header('hmac'); $data = $request->json()->all(); $status = strtolower($data['status'] ?? 'unknown'); @@ -335,6 +343,15 @@ class OxapayProvider implements PaymentProviderContract if (! $trackId) { throw new \Exception('Missing track_id in webhook payload'); } + Log::debug('before send webhook status'); + // Notify webhook received + $this->notifyWebhookReceived($status, $trackId); + Log::debug('before send webhook status with payload'.$status.' '.$trackId); + + if (! $this->validateWebhook($request)) { + $this->notifyWebhookError($status, $trackId, 'Invalid webhook signature'); + throw new \Exception('Invalid webhook signature'); + } Log::info('OxaPay webhook received', [ 'track_id' => $trackId, @@ -345,6 +362,9 @@ class OxapayProvider implements PaymentProviderContract // Process the webhook based on status $result = $this->handleWebhookStatus($status, $data); + // Notify webhook processed successfully + $this->notifyWebhookProcessed($status, $trackId, $data['payment_id'] ?? null); + return [ 'success' => true, 'event_type' => $status, @@ -361,6 +381,9 @@ class OxapayProvider implements PaymentProviderContract 'payload' => $request->getContent(), ]); + // Notify webhook processing error + $this->notifyWebhookError($data['status'] ?? 'unknown', $data['track_id'] ?? 'unknown', $e->getMessage()); + return [ 'success' => false, 'event_type' => 'error', @@ -543,6 +566,15 @@ class OxapayProvider implements PaymentProviderContract $subscription->update($updateData); + // Notify payment success for paid payments + if (in_array($status, ['paid', 'manual_accept'])) { + $this->notifyPaymentSuccess($subscription->user, $subscription->plan, [ + 'amount' => $data['amount'] ?? 0, + 'currency' => $data['currency'] ?? 'USD', + 'transaction_id' => $trackId, + ]); + } + Log::info('OxaPay subscription updated', [ 'subscription_id' => $subscription->id, 'track_id' => $trackId, @@ -824,4 +856,109 @@ class OxapayProvider implements PaymentProviderContract { throw new \Exception('OxaPay does not support recurring subscriptions'); } + + // Notification methods + protected function notifyPaymentSuccess(User $user, Plan $plan, array $paymentDetails): void + { + $message = "πŸ’° PAYMENT SUCCESS\n". + "πŸ“§ Customer: {$user->name} ({$user->email})\n". + "πŸ“¦ Plan: {$plan->name}\n". + 'πŸ’΅ Amount: $'.number_format($paymentDetails['amount'] ?? 0, 2).' '.($paymentDetails['currency'] ?? 'USD')."\n". + "πŸͺ Provider: OxaPay\n". + 'πŸ†” Transaction: '.($paymentDetails['transaction_id'] ?? 'N/A')."\n". + '⏰ Time: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifyWebhookReceived(string $eventType, ?string $webhookId): void + { + $message = "πŸ”” WEBHOOK RECEIVED\n". + "πŸͺ Provider: OxaPay\n". + "πŸ“‹ Event: {$eventType}\n". + 'πŸ†” Webhook ID: '.($webhookId ?? 'N/A')."\n". + "πŸ“Š Status: Processing...\n". + '⏰ Received: '.now()->format('Y-m-d H:i:s'); + + Log::warning($message); + + $res = $this->sendTelegramNotification($message); + + Log::warning('result : '.$res); + } + + protected function notifyWebhookProcessed(string $eventType, ?string $webhookId, ?string $paymentId = null): void + { + $message = "βœ… WEBHOOK PROCESSED\n". + "πŸͺ Provider: OxaPay\n". + "πŸ“‹ Event: {$eventType}\n". + 'πŸ†” Webhook ID: '.($webhookId ?? 'N/A')."\n". + ($paymentId ? "πŸ’³ Payment ID: {$paymentId}\n" : ''). + "πŸ“Š Status: Completed\n". + '⏰ Processed: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifyWebhookError(string $eventType, ?string $webhookId, string $error): void + { + $message = "❌ WEBHOOK ERROR\n". + "πŸͺ Provider: OxaPay\n". + "πŸ“‹ Event: {$eventType}\n". + 'πŸ†” Webhook ID: '.($webhookId ?? 'N/A')."\n". + "πŸ’₯ Error: {$error}\n". + '⏰ Time: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifyProviderError(string $operation, string $error, array $context = []): void + { + $contextStr = ''; + if (! empty($context)) { + $contextStr = 'πŸ“ Details: '.json_encode(array_slice($context, 0, 3, true), JSON_UNESCAPED_SLASHES)."\n"; + if (count($context) > 3) { + $contextStr .= 'πŸ“ Additional: '.(count($context) - 3).' more items'."\n"; + } + } + + $message = "🚨 PROVIDER ERROR\n". + "πŸͺ Provider: OxaPay\n". + "πŸ“‘ Operation: {$operation}\n". + "πŸ’₯ Error: {$error}\n". + $contextStr. + '⏰ Time: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifyPaymentCreated(User $user, Plan $plan, array $paymentDetails): void + { + $message = "πŸ†• PAYMENT CREATED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + 'πŸ’° Amount: $'.number_format($paymentDetails['amount'] ?? 0, 2).' '.($paymentDetails['currency'] ?? 'USD')."\n". + "πŸͺ Provider: OxaPay\n". + 'πŸ†” Payment ID: '.($paymentDetails['payment_id'] ?? 'N/A')."\n". + 'πŸ“… Created: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifySubscriptionCancelled(Subscription $subscription, string $reason): void + { + $user = $subscription->user; + $plan = $subscription->plan; + + $message = "❌ SUBSCRIPTION CANCELLED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + "πŸͺ Provider: OxaPay\n". + "πŸ’­ Reason: {$reason}\n". + "πŸ†” Subscription ID: {$subscription->provider_subscription_id}\n". + ($subscription->ends_at ? 'πŸ“… Effective: '.$subscription->ends_at->format('Y-m-d')."\n" : ''). + '⏰ Cancelled: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } } diff --git a/app/Services/Payments/Providers/PolarProvider.php b/app/Services/Payments/Providers/PolarProvider.php index ba21f81..fce8a5b 100644 --- a/app/Services/Payments/Providers/PolarProvider.php +++ b/app/Services/Payments/Providers/PolarProvider.php @@ -7,6 +7,7 @@ use App\Models\PaymentProvider as PaymentProviderModel; use App\Models\Plan; use App\Models\Subscription; use App\Models\User; +use App\NotifyMe; use App\Services\Webhooks\WebhookFactory; use App\Services\Webhooks\WebhookVerificationException; use Carbon\Carbon; @@ -16,6 +17,8 @@ use Illuminate\Support\Facades\Log; class PolarProvider implements PaymentProviderContract { + use NotifyMe; + protected array $config; protected bool $sandbox; @@ -136,32 +139,48 @@ class PolarProvider implements PaymentProviderContract public function makeAuthenticatedRequest(string $method, string $endpoint, array $data = []): \Illuminate\Http\Client\Response { - $url = $this->apiBaseUrl.$endpoint; + try { + $url = $this->apiBaseUrl.$endpoint; - $headers = [ - 'Authorization' => 'Bearer '.$this->apiKey, - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ]; + $headers = [ + 'Authorization' => 'Bearer '.$this->apiKey, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; - $http = Http::withHeaders($headers) - ->timeout(30) - ->withOptions([ - 'verify' => true, - 'curl' => [ - CURLOPT_SSL_VERIFYPEER => true, - CURLOPT_SSL_VERIFYHOST => 2, - CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2, - ], - ]); + $http = Http::withHeaders($headers) + ->timeout(30) + ->withOptions([ + 'verify' => true, + 'curl' => [ + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2, + ], + ]); - return match ($method) { - 'GET' => $http->get($url, $data), - 'POST' => $http->post($url, $data), - 'PATCH' => $http->patch($url, $data), - 'DELETE' => $http->delete($url, $data), - default => throw new \InvalidArgumentException("Unsupported HTTP method: {$method}"), - }; + $response = match ($method) { + 'GET' => $http->get($url, $data), + 'POST' => $http->post($url, $data), + 'PATCH' => $http->patch($url, $data), + 'DELETE' => $http->delete($url, $data), + default => throw new \InvalidArgumentException("Unsupported HTTP method: {$method}"), + }; + + // Log API errors + if (! $response->successful()) { + $this->notifyProviderError("{$method} {$endpoint}", "HTTP {$response->status()}", [ + 'status_code' => $response->status(), + 'response' => $response->json(), + ]); + } + + return $response; + + } catch (\Exception $e) { + $this->notifyProviderError("{$method} {$endpoint}", $e->getMessage()); + throw $e; + } } public function createSubscription(User $user, Plan $plan, array $options = []): array @@ -298,6 +317,9 @@ class PolarProvider implements PaymentProviderContract 'expires_at' => $checkout['expires_at'] ?? now()->addHours(24)->toISOString(), ]; + // Notify checkout created successfully + $this->notifyCheckoutCreated($user, $plan, $checkout); + Log::info('PolarProvider: Returning successful result', [ 'result_keys' => array_keys($result), 'checkout_url' => $result['checkout_url'], @@ -329,6 +351,9 @@ class PolarProvider implements PaymentProviderContract 'cancellation_reason' => $reason, ]); + // Notify subscription cancelled + $this->notifySubscriptionCancelled($subscription, $reason ?: 'Local cancellation'); + return true; } @@ -347,6 +372,9 @@ class PolarProvider implements PaymentProviderContract 'cancellation_reason' => $reason, ]); + // Notify subscription cancelled + $this->notifySubscriptionCancelled($subscription, $reason ?: 'Manual cancellation via API'); + return true; } catch (\Exception $e) { @@ -594,13 +622,25 @@ class PolarProvider implements PaymentProviderContract $signature = $request->header('Polar-Signature'); if (! $this->validateWebhook($request)) { + $webhookId = $request->header('webhook-id'); // Extract from header for error reporting too Log::error('Invalid Polar webhook signature'); + $this->notifyWebhookError($webhookData['type'] ?? 'unknown', $webhookId, 'Invalid webhook signature'); throw new \Exception('Invalid webhook signature'); } $webhookData = json_decode($payload, true); $eventType = $webhookData['type'] ?? 'unknown'; - $webhookId = $webhookData['id'] ?? null; + $webhookId = $request->header('webhook-id'); // Extract from header as per Polar documentation + + Log::info('Polar webhook received', [ + 'event_type' => $eventType, + 'webhook_id' => $webhookId, + 'webhook_id_header' => $request->header('webhook-id'), + 'available_headers' => $request->headers->all(), + ]); + + // Notify webhook received + $this->notifyWebhookReceived($eventType, $webhookId, $webhookData); // Check for idempotency - prevent duplicate processing if ($webhookId && $this->isWebhookProcessed($webhookId)) { @@ -676,6 +716,10 @@ class PolarProvider implements PaymentProviderContract // Mark webhook as processed if it has an ID and was successfully processed if ($webhookId && ($result['processed'] ?? false)) { $this->markWebhookAsProcessed($webhookId, $eventType, $result); + + // Notify webhook processed successfully + $subscriptionId = $result['data']['subscription_id'] ?? null; + $this->notifyWebhookProcessed($eventType, $webhookId, $subscriptionId); } return $result; @@ -687,6 +731,10 @@ class PolarProvider implements PaymentProviderContract 'event_type' => $eventType ?? 'unknown', 'payload' => $request->getContent(), ]); + + // Notify webhook processing error + $this->notifyWebhookError($eventType ?? 'unknown', $webhookId ?? 'unknown', $e->getMessage()); + throw $e; } } @@ -903,6 +951,325 @@ class PolarProvider implements PaymentProviderContract return true; } + // Notification methods + protected function notifyPaymentSuccess(User $user, Plan $plan, array $paymentDetails): void + { + $message = "πŸ’° PAYMENT SUCCESS\n". + "πŸ“§ Customer: {$user->name} ({$user->email})\n". + "πŸ“¦ Plan: {$plan->name}\n". + 'πŸ’΅ Amount: $'.number_format($paymentDetails['amount'] ?? 0, 2).' '.($paymentDetails['currency'] ?? 'USD')."\n". + "πŸͺ Provider: Polar\n". + 'πŸ†” Transaction: '.($paymentDetails['transaction_id'] ?? 'N/A')."\n". + '⏰ Time: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifySubscriptionCreated(User $user, Plan $plan, Subscription $subscription): void + { + $amount = $plan->price ? '$'.number_format($plan->price, 2).'/month' : 'Custom pricing'; + + $message = "πŸ†• SUBSCRIPTION CREATED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + "πŸ’° Billing: {$amount}\n". + "πŸͺ Provider: Polar\n". + "πŸ”„ Subscription ID: {$subscription->provider_subscription_id}\n". + 'πŸ“… Created: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifyWebhookReceived(string $eventType, ?string $webhookId, ?array $webhookData = null): void + { + $message = "πŸ”” WEBHOOK RECEIVED\n". + "πŸͺ Provider: Polar\n". + "πŸ“‹ Event: {$eventType}\n". + 'πŸ†” Webhook ID: '.($webhookId ?? 'N/A')."\n"; + + // Try to extract user information safely + try { + $userInfo = $this->extractUserInfoFromWebhook($webhookData); + if ($userInfo['name'] || $userInfo['email']) { + $message .= 'πŸ‘€ User: '; + if ($userInfo['name']) { + $message .= $userInfo['name']; + if ($userInfo['email']) { + $message .= " ({$userInfo['email']})"; + } + } elseif ($userInfo['email']) { + $message .= $userInfo['email']; + } + $message .= "\n"; + } + } catch (\Exception $e) { + // Silently ignore user info extraction errors to avoid webhook processing failures + Log::debug('Failed to extract user info for webhook notification', [ + 'event_type' => $eventType, + 'webhook_id' => $webhookId, + 'error' => $e->getMessage(), + ]); + } + + $message .= "πŸ“Š Status: Processing...\n". + '⏰ Received: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function extractUserInfoFromWebhook(?array $webhookData): array + { + $userInfo = [ + 'name' => null, + 'email' => null, + ]; + + if (! $webhookData) { + return $userInfo; + } + + // Try different paths where user info might be stored in webhook payloads + $data = $webhookData['data'] ?? []; + + // 1. Check customer object (most common) + if (isset($data['customer'])) { + $customer = $data['customer']; + if (is_array($customer)) { + $userInfo['name'] = $customer['name'] ?? $customer['display_name'] ?? null; + $userInfo['email'] = $customer['email'] ?? null; + } + } + + // 2. Check user object (some webhooks use this) + if (! $userInfo['name'] && ! $userInfo['email'] && isset($data['user'])) { + $user = $data['user']; + if (is_array($user)) { + $userInfo['name'] = $user['name'] ?? $user['display_name'] ?? null; + $userInfo['email'] = $user['email'] ?? null; + } + } + + // 3. Check direct customer fields + if (! $userInfo['name'] && ! $userInfo['email']) { + $userInfo['name'] = $data['customer_name'] ?? $data['customer_display_name'] ?? null; + $userInfo['email'] = $data['customer_email'] ?? $data['email'] ?? null; + } + + // 4. Check metadata for user info + if (! $userInfo['name'] && ! $userInfo['email'] && isset($data['metadata'])) { + $metadata = $data['metadata']; + if (is_array($metadata)) { + $userInfo['name'] = $metadata['user_name'] ?? $metadata['customer_name'] ?? null; + $userInfo['email'] = $metadata['user_email'] ?? $metadata['customer_email'] ?? null; + } + } + + // 5. For order-related webhooks, check order data + if (! $userInfo['name'] && ! $userInfo['email'] && isset($data['order'])) { + $order = $data['order']; + if (is_array($order)) { + $userInfo['name'] = $order['customer_name'] ?? $order['user_name'] ?? null; + $userInfo['email'] = $order['customer_email'] ?? $order['user_email'] ?? null; + + // Check nested customer in order + if (! $userInfo['name'] && ! $userInfo['email'] && isset($order['customer'])) { + $orderCustomer = $order['customer']; + if (is_array($orderCustomer)) { + $userInfo['name'] = $orderCustomer['name'] ?? $orderCustomer['display_name'] ?? null; + $userInfo['email'] = $orderCustomer['email'] ?? null; + } + } + } + } + + // 6. Check subscription data for user info + if (! $userInfo['name'] && ! $userInfo['email'] && isset($data['subscription'])) { + $subscription = $data['subscription']; + if (is_array($subscription)) { + $userInfo['name'] = $subscription['customer_name'] ?? null; + $userInfo['email'] = $subscription['customer_email'] ?? null; + + // Check nested customer in subscription + if (! $userInfo['name'] && ! $userInfo['email'] && isset($subscription['customer'])) { + $subCustomer = $subscription['customer']; + if (is_array($subCustomer)) { + $userInfo['name'] = $subCustomer['name'] ?? $subCustomer['display_name'] ?? null; + $userInfo['email'] = $subCustomer['email'] ?? null; + } + } + } + } + + return $userInfo; + } + + protected function notifyWebhookProcessed(string $eventType, ?string $webhookId, ?string $subscriptionId = null): void + { + $message = "βœ… WEBHOOK PROCESSED\n". + "πŸͺ Provider: Polar\n". + "πŸ“‹ Event: {$eventType}\n". + 'πŸ†” Webhook ID: '.($webhookId ?? 'N/A')."\n". + ($subscriptionId ? "πŸ”„ Subscription ID: {$subscriptionId}\n" : ''). + "πŸ“Š Status: Completed\n". + '⏰ Processed: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifyWebhookError(string $eventType, ?string $webhookId, string $error): void + { + $message = "❌ WEBHOOK ERROR\n". + "πŸͺ Provider: Polar\n". + "πŸ“‹ Event: {$eventType}\n". + 'πŸ†” Webhook ID: '.($webhookId ?? 'N/A')."\n". + "πŸ’₯ Error: {$error}\n". + '⏰ Time: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifySubscriptionUpdated(Subscription $subscription, string $changeType, $details = null): void + { + $user = $subscription->user; + $plan = $subscription->plan; + + $message = "πŸ”„ SUBSCRIPTION UPDATED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + "πŸͺ Provider: Polar\n". + "πŸ”„ Change: {$changeType}\n". + ($details ? "πŸ“ Details: {$details}\n" : ''). + "πŸ†” Subscription ID: {$subscription->provider_subscription_id}\n". + '⏰ Updated: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifySubscriptionCancelled(Subscription $subscription, string $reason): void + { + $user = $subscription->user; + $plan = $subscription->plan; + + $message = "❌ SUBSCRIPTION CANCELLED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + "πŸͺ Provider: Polar\n". + "πŸ’­ Reason: {$reason}\n". + "πŸ†” Subscription ID: {$subscription->provider_subscription_id}\n". + ($subscription->ends_at ? 'πŸ“… Effective: '.$subscription->ends_at->format('Y-m-d')."\n" : ''). + '⏰ Cancelled: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifyProviderError(string $operation, string $error, array $context = []): void + { + $contextStr = ''; + if (! empty($context)) { + $contextStr = 'πŸ“ Details: '.json_encode(array_slice($context, 0, 3, true), JSON_UNESCAPED_SLASHES)."\n"; + if (count($context) > 3) { + $contextStr .= 'πŸ“ Additional: '.(count($context) - 3).' more items'."\n"; + } + } + + $message = "🚨 PROVIDER ERROR\n". + "πŸͺ Provider: Polar\n". + "πŸ“‘ Operation: {$operation}\n". + "πŸ’₯ Error: {$error}\n". + $contextStr. + '⏰ Time: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifySubscriptionActivated(User $user, Plan $plan, Subscription $subscription): void + { + $amount = $plan->price ? '$'.number_format($plan->price, 2).'/month' : 'Custom pricing'; + + $message = "βœ… SUBSCRIPTION ACTIVATED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + "πŸ’° Billing: {$amount}\n". + "πŸͺ Provider: Polar\n". + "πŸ”„ Subscription ID: {$subscription->provider_subscription_id}\n". + 'πŸ“… Activated: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifySubscriptionPaused(User $user, Plan $plan, Subscription $subscription, string $reason): void + { + $amount = $plan->price ? '$'.number_format($plan->price, 2).'/month' : 'Custom pricing'; + + $message = "⏸️ SUBSCRIPTION PAUSED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + "πŸ’° Billing: {$amount}\n". + "πŸͺ Provider: Polar\n". + "πŸ”„ Subscription ID: {$subscription->provider_subscription_id}\n". + "πŸ’­ Reason: {$reason}\n". + 'πŸ“… Paused: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifySubscriptionResumed(User $user, Plan $plan, Subscription $subscription, string $reason): void + { + $amount = $plan->price ? '$'.number_format($plan->price, 2).'/month' : 'Custom pricing'; + + $message = "▢️ SUBSCRIPTION RESUMED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + "πŸ’° Billing: {$amount}\n". + "πŸͺ Provider: Polar\n". + "πŸ”„ Subscription ID: {$subscription->provider_subscription_id}\n". + "πŸ’­ Reason: {$reason}\n". + 'πŸ“… Resumed: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifyCheckoutCreated(User $user, Plan $plan, array $checkout): void + { + $amount = $plan->price ? '$'.number_format($plan->price, 2).'/month' : 'Custom pricing'; + + $customerId = $checkout['customer_id'] ?? 'N/A'; + + $message = "πŸ›’ CHECKOUT CREATED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + "πŸ’° Billing: {$amount}\n". + "πŸͺ Provider: Polar\n". + "πŸ†” Checkout ID: {$checkout['id']}\n". + "πŸ”— Checkout URL: {$checkout['url']}\n". + "πŸ‘€ Customer ID: {$customerId}\n". + '⏰ Created: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + + protected function notifyOrderCreated(User $user, Plan $plan, array $order): void + { + $amount = isset($order['amount']) ? '$'.number_format($order['amount'] / 100, 2) : 'N/A'; + $currency = $order['currency'] ?? 'USD'; + $checkoutId = $order['checkout_id'] ?? 'N/A'; + $customerId = $order['customer_id'] ?? 'N/A'; + $status = $order['status'] ?? 'unknown'; + + $message = "πŸ“¦ ORDER CREATED\n". + "πŸ‘€ User: {$user->name} ({$user->email})\n". + "πŸ“‹ Plan: {$plan->name}\n". + "πŸ’° Amount: {$amount} {$currency}\n". + "πŸͺ Provider: Polar\n". + "πŸ†” Order ID: {$order['id']}\n". + "πŸ›’ Checkout ID: {$checkoutId}\n". + "πŸ‘€ Customer ID: {$customerId}\n". + "πŸ“Š Status: {$status}\n". + '⏰ Created: '.now()->format('Y-m-d H:i:s'); + + $this->sendTelegramNotification($message); + } + // Helper methods protected function getOrCreateCustomer(User $user): array { @@ -1225,6 +1592,9 @@ class PolarProvider implements PaymentProviderContract 'order_created_at' => now()->toISOString(), ]), ]); + + // Notify order created + $this->notifyOrderCreated($subscription->user, $subscription->plan, $order); } return [ @@ -1260,6 +1630,13 @@ class PolarProvider implements PaymentProviderContract 'polar_order' => $order, ]), ]); + + // Notify payment success + $this->notifyPaymentSuccess($subscription->user, $subscription->plan, [ + 'amount' => $order['amount'] / 100, // Convert from cents + 'currency' => $order['currency'] ?? 'USD', + 'transaction_id' => $order['id'], + ]); } return [ @@ -1308,6 +1685,9 @@ class PolarProvider implements PaymentProviderContract $localSubscription->update($updateData); + // Notify subscription created + $this->notifySubscriptionCreated($localSubscription->user, $localSubscription->plan, $localSubscription); + Log::info('Polar subscription created/updated via webhook', [ 'local_subscription_id' => $localSubscription->id, 'polar_subscription_id' => $polarSubscription['id'], @@ -1364,6 +1744,9 @@ class PolarProvider implements PaymentProviderContract $localSubscription->update($updateData); + // Notify subscription activated + $this->notifySubscriptionActivated($localSubscription->user, $localSubscription->plan, $localSubscription); + Log::info('Polar subscription activated via webhook', [ 'local_subscription_id' => $localSubscription->id, 'polar_subscription_id' => $polarSubscription['id'], @@ -1426,11 +1809,6 @@ class PolarProvider implements PaymentProviderContract $updateData['ends_at'] = Carbon::parse($polarSubscription['ends_at']); } - // Handle cancellation details - if (isset($polarSubscription['cancel_at_period_end'])) { - $updateData['cancel_at_period_end'] = $polarSubscription['cancel_at_period_end']; - } - // Set cancellation reason if provided if (! empty($polarSubscription['customer_cancellation_reason'])) { $updateData['cancellation_reason'] = $polarSubscription['customer_cancellation_reason']; @@ -1445,7 +1823,7 @@ class PolarProvider implements PaymentProviderContract 'updated_fields' => array_keys($updateData), 'cancelled_at_updated' => isset($updateData['cancelled_at']), 'cancellation_reason_updated' => isset($updateData['cancellation_reason']), - 'cancel_at_period_end_updated' => isset($updateData['cancel_at_period_end']), + 'cancel_at_period_end_stored' => ($updateData['provider_data']['cancel_at_period_end'] ?? false), ]); } else { Log::warning('Subscription not found for Polar subscription.updated webhook', [ @@ -1503,11 +1881,6 @@ class PolarProvider implements PaymentProviderContract ]), ]; - // Set cancel_at_period_end flag if provided - if (isset($polarSubscription['cancel_at_period_end'])) { - $updateData['cancel_at_period_end'] = $polarSubscription['cancel_at_period_end']; - } - // Use Polar's cancellation timestamp if available, otherwise use now if (! empty($polarSubscription['cancelled_at'])) { $updateData['cancelled_at'] = Carbon::parse($polarSubscription['cancelled_at']); @@ -1528,6 +1901,9 @@ class PolarProvider implements PaymentProviderContract $localSubscription->update($updateData); + // Notify subscription cancelled + $this->notifySubscriptionCancelled($localSubscription, $cancellationReason); + Log::info('Polar subscription cancellation processed via webhook', [ 'local_subscription_id' => $localSubscription->id, 'polar_subscription_id' => $polarSubscription['id'], @@ -1597,6 +1973,9 @@ class PolarProvider implements PaymentProviderContract ]), ]); + // Notify subscription paused + $this->notifySubscriptionPaused($localSubscription->user, $localSubscription->plan, $localSubscription, $polarSubscription['pause_reason'] ?? 'Manual pause'); + Log::info('Polar subscription paused', [ 'local_subscription_id' => $localSubscription->id, 'polar_subscription_id' => $polarSubscription['id'], @@ -1669,6 +2048,9 @@ class PolarProvider implements PaymentProviderContract ]), ]); + // Notify subscription resumed + $this->notifySubscriptionResumed($localSubscription->user, $localSubscription->plan, $localSubscription, $polarSubscription['resume_reason'] ?? 'Manual resume'); + Log::info('Polar subscription resumed', [ 'local_subscription_id' => $localSubscription->id, 'polar_subscription_id' => $polarSubscription['id'],