feat(notifications): implement comprehensive telegram notifications for payment providers
- Add NotifyMe trait with centralized Telegram bot integration - Implement webhook notifications for Polar, OxaPay, and ActivationKey providers - Add subscription lifecycle notifications (create, activate, cancel, pause, resume) - Enhance Polar webhook processing with user context and error handling - Fix subscription.updated and subscription.canceled webhook column errors - Add idempotent webhook processing to prevent duplicate handling
This commit is contained in:
@@ -65,6 +65,8 @@ class Subscription extends Model
|
||||
'amount',
|
||||
'currency',
|
||||
'expires_at',
|
||||
'uncanceled_at',
|
||||
'cancel_at_period_end',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +139,7 @@ class PolarProvider implements PaymentProviderContract
|
||||
|
||||
public function makeAuthenticatedRequest(string $method, string $endpoint, array $data = []): \Illuminate\Http\Client\Response
|
||||
{
|
||||
try {
|
||||
$url = $this->apiBaseUrl.$endpoint;
|
||||
|
||||
$headers = [
|
||||
@@ -155,13 +159,28 @@ class PolarProvider implements PaymentProviderContract
|
||||
],
|
||||
]);
|
||||
|
||||
return match ($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'],
|
||||
|
||||
Reference in New Issue
Block a user