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:
idevakk
2025-12-08 09:25:19 -08:00
parent 8d8cd44ea5
commit 0d33c57b32
5 changed files with 654 additions and 47 deletions

View File

@@ -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);
}
}