loadConfigurationFromModel(); // Merge with any passed config (passed config takes precedence) $config = array_merge($dbConfig, $config); $this->config = $config; $this->sandbox = (bool) ($config['sandbox'] ?? false); $this->merchantApiKey = $this->sandbox ? ($config['sandbox_merchant_api_key'] ?? '') : ($config['merchant_api_key'] ?? ''); $this->baseUrl = 'https://api.oxapay.com/v1'; Log::info('OxaPayProvider configuration loaded', [ 'sandbox' => $this->sandbox, 'has_merchant_api_key' => ! empty($this->merchantApiKey), 'base_url' => $this->baseUrl, ]); } /** * Load configuration from PaymentProvider model */ protected function loadConfigurationFromModel(): array { try { $providerModel = PaymentProviderModel::where('name', 'oxapay')->first(); if ($providerModel && $providerModel->configuration) { return $providerModel->configuration; } Log::warning('OxaPayProvider configuration not found in database, using defaults'); return $this->getDefaultConfiguration(); } catch (\Exception $e) { Log::error('OxaPayProvider failed to load configuration from database', [ 'error' => $e->getMessage(), ]); return $this->getDefaultConfiguration(); } } /** * Get default configuration */ protected function getDefaultConfiguration(): array { return [ 'merchant_api_key' => '', 'sandbox_merchant_api_key' => '', 'sandbox' => false, 'webhook_url' => route('webhook.payment', 'oxapay'), 'success_url' => route('payment.success'), 'cancel_url' => route('payment.cancel'), 'default_lifetime' => 60, // minutes 'default_under_paid_coverage' => 5, // percentage 'fee_paid_by_payer' => 0, // merchant pays by default ]; } public function getName(): string { return 'oxapay'; } public function isActive(): bool { return ! empty($this->merchantApiKey); } public function supportsRecurring(): bool { return false; // OxaPay doesn't support recurring payments } public function supportsOneTime(): bool { return true; } public function getSupportedCurrencies(): array { return Cache::remember('oxapay_currencies', now()->addHour(), function () { try { $response = Http::withHeaders([ 'merchant_api_key' => $this->merchantApiKey, ])->get("{$this->baseUrl}/info/currencies"); if ($response->successful()) { $data = $response->json(); return $data['data'] ?? []; } } catch (\Exception $e) { Log::error('Failed to fetch OxaPay currencies', [ 'error' => $e->getMessage(), ]); } return ['BTC', 'ETH', 'USDT', 'USDC', 'LTC', 'BCH']; // Default common cryptos }); } public function calculateFees(float $amount): array { // OxaPay fees vary by currency and network // Using average estimates - actual fees should be fetched from API $percentageFee = 0.5; // 0.5% average $fixedFee = 0.0; // No fixed fee for most cryptos $totalFee = ($amount * $percentageFee / 100) + $fixedFee; return [ 'fixed_fee' => $fixedFee, 'percentage_fee' => $percentageFee, 'total_fee' => $totalFee, 'net_amount' => $amount - $totalFee, ]; } public function createSubscription(User $user, Plan $plan, array $options = []): array { throw new \Exception('OxaPay does not support recurring subscriptions'); } public function cancelSubscription(Subscription $subscription, string $reason = ''): bool { try { // For oxapay, we don't actually cancel since it's a one-time activation // We can deactivate the subscription if needed $subscription->update([ 'status' => 'cancelled', 'cancelled_at' => now(), 'cancellation_reason' => $reason, ]); // Notify subscription cancelled $this->notifySubscriptionCancelled($subscription, $reason ?: 'Manual cancellation'); return true; } catch (\Exception $e) { Log::error('Oxapay subscription cancellation failed', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); throw $e; } } public function updateSubscription(Subscription $subscription, Plan $newPlan): array { throw new \Exception('OxaPay does not support recurring subscriptions'); } public function pauseSubscription(Subscription $subscription): bool { throw new \Exception('OxaPay does not support recurring subscriptions'); } public function resumeSubscription(Subscription $subscription): bool { throw new \Exception('OxaPay does not support recurring subscriptions'); } public function getSubscriptionDetails(string $subscriptionId): array { throw new \Exception('OxaPay does not support recurring subscriptions'); } public function createCheckoutSession(User $user, Plan $plan, array $options = []): array { try { $amount = (float) $plan->price; $currency = (string) ($this->config['currency'] ?? 'USD'); // Build simple invoice request payload $payload = [ 'amount' => $amount, // number · decimal 'currency' => $currency, // string 'lifetime' => (int) ($this->config['lifetime'] ?? 30), // integer · min: 15 · max: 2880 'fee_paid_by_payer' => (int) ($this->config['fee_paid_by_payer'] ?? 1), // number · decimal · max: 1 'under_paid_coverage' => (float) ($this->config['under_paid_coverage'] ?? 2.5), // number · decimal · max: 60 'auto_withdrawal' => (bool) ($this->config['auto_withdrawal'] ?? false), // boolean 'mixed_payment' => (bool) ($this->config['mixed_payment'] ?? false), // boolean 'return_url' => (string) ($this->config['success_url'] ?? route('payment.success')), // string 'callback_url' => (string) ($this->config['callback_url'] ?? route('webhook.oxapay')), // string 'order_id' => (string) ($this->config['order_id'] ?? 'order_'.$user->id.'_'.time()), // string 'thanks_message' => (string) ($this->config['thanks_message'] ?? 'Thank you for your payment!'), // string 'description' => (string) ($this->config['description'] ?? "Payment for plan: {$plan->name}"), // string 'sandbox' => $this->sandbox, // boolean ]; // Add to_currency only if it's properly configured $configuredToCurrency = $this->config['to_currency'] ?? null; if (! empty($configuredToCurrency)) { $toCurrency = (string) $configuredToCurrency; $payload['to_currency'] = $toCurrency; } Log::info('Creating OxaPay invoice', [ 'user_id' => $user->id, 'plan_id' => $plan->id, 'payload_keys' => array_keys($payload), ]); $response = Http::withHeaders([ 'merchant_api_key' => $this->merchantApiKey, 'Content-Type' => 'application/json', ])->post("{$this->baseUrl}/payment/invoice", $payload); if (! $response->successful()) { $errorData = $response->json(); $errorMessage = $errorData['message'] ?? 'Unknown error'; Log::error('OxaPay invoice creation failed', [ 'status' => $response->status(), 'response' => $errorData, 'payload' => $payload, ]); throw new \Exception("Failed to create OxaPay invoice: {$errorMessage}"); } $data = $response->json(); if (! isset($data['data']) || $data['status'] !== 200) { throw new \Exception('Invalid response from OxaPay API'); } $paymentData = $data['data']; // Create local subscription record $subscription = Subscription::create([ 'user_id' => $user->id, 'plan_id' => $plan->id, 'provider' => 'oxapay', 'type' => 'one_time', // OxaPay doesn't support recurring 'provider_subscription_id' => $paymentData['track_id'], 'provider_checkout_id' => $paymentData['track_id'], 'status' => 'pending_payment', 'unified_status' => 'pending_payment', 'quantity' => 1, 'provider_data' => $paymentData, 'starts_at' => now(), 'ends_at' => $this->calculateSubscriptionEndDate($plan), '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'], 'user_id' => $user->id, 'plan_id' => $plan->id, ]); return [ 'success' => true, 'checkout_url' => $paymentData['payment_url'] ?? null, 'payment_id' => $paymentData['track_id'] ?? null, 'track_id' => $paymentData['track_id'] ?? null, 'subscription_id' => $subscription->id, 'amount' => $paymentData['amount'] ?? $amount, 'currency' => $paymentData['currency'] ?? $currency, 'pay_amount' => $paymentData['pay_amount'] ?? null, 'pay_currency' => $paymentData['pay_currency'] ?? null, 'expires_at' => isset($paymentData['expired_at']) ? Carbon::createFromTimestamp($paymentData['expired_at']) : null, 'lifetime' => $paymentData['lifetime'] ?? $payload['lifetime'], 'under_paid_coverage' => $paymentData['under_paid_coverage'] ?? $payload['under_paid_coverage'], 'fee_paid_by_payer' => $paymentData['fee_paid_by_payer'] ?? $payload['fee_paid_by_payer'], 'provider' => 'oxapay', 'provider_data' => $paymentData, ]; } catch (\Exception $e) { Log::error('OxaPay checkout session creation failed', [ 'error' => $e->getMessage(), 'user_id' => $user->id, 'plan_id' => $plan->id, ]); return [ 'success' => false, 'error' => $e->getMessage(), ]; } } public function createCustomerPortalSession(User $user): array { throw new \Exception('OxaPay does not provide customer portal functionality'); } public function processWebhook(Request $request): array { try { $payload = $request->getContent(); $signature = $request->header('hmac'); $data = $request->json()->all(); $status = strtolower($data['status'] ?? 'unknown'); $trackId = $data['track_id'] ?? null; $type = $data['type'] ?? 'payment'; 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, 'status' => $status, 'type' => $type, ]); // 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, 'provider_transaction_id' => $trackId, 'processed' => true, 'data' => $data, 'type' => $type, 'result' => $result, ]; } catch (\Exception $e) { Log::error('OxaPay webhook processing failed', [ 'error' => $e->getMessage(), 'payload' => $request->getContent(), ]); // Notify webhook processing error $this->notifyWebhookError($data['status'] ?? 'unknown', $data['track_id'] ?? 'unknown', $e->getMessage()); return [ 'success' => false, 'event_type' => 'error', 'provider_transaction_id' => null, 'processed' => false, 'error' => $e->getMessage(), ]; } } public function validateWebhook(Request $request): bool { try { $payload = $request->getContent(); $signature = $request->header('hmac'); // Use lowercase 'hmac' as per official implementation if (empty($signature)) { Log::warning('OxaPay webhook validation failed: missing HMAC header'); return false; } // Get webhook data to determine type $data = $request->json()->all(); $type = $data['type'] ?? ''; // Resolve API key based on webhook type (following official implementation) $apiKey = $this->resolveApiKeyByType($type); if (empty($apiKey)) { Log::warning('OxaPay webhook validation failed: no API key available for type', [ 'type' => $type, ]); return false; } $expectedSignature = hash_hmac('sha512', $payload, $apiKey); $isValid = hash_equals($expectedSignature, $signature); Log::info('OxaPay webhook validation', [ 'type' => $type, 'signature_provided' => ! empty($signature), 'api_key_available' => ! empty($apiKey), 'valid' => $isValid, ]); return $isValid; } catch (\Exception $e) { Log::error('OxaPay webhook validation failed', [ 'error' => $e->getMessage(), ]); return false; } } /** * Resolve API key based on webhook type (following official OxaPay implementation) */ private function resolveApiKeyByType(string $type): string { // Map webhook types to API key groups per official implementation $merchantTypes = ['invoice', 'white_label', 'static_address', 'payment_link', 'donation']; $payoutTypes = ['payout']; if (in_array($type, $merchantTypes, true)) { return $this->config['merchant_api_key'] ?? ''; } if (in_array($type, $payoutTypes, true)) { return $this->config['payout_api_key'] ?? ''; } // Default to merchant API key for unknown types return $this->config['merchant_api_key'] ?? ''; } /** * Handle different webhook statuses */ protected function handleWebhookStatus(string $status, array $data): array { $trackId = $data['track_id']; $orderId = $data['order_id'] ?? null; switch ($status) { case 'paying': // Payment is being processed, show as pending return $this->updatePaymentStatus($trackId, 'paying', $data); case 'paid': // Payment completed successfully return $this->updatePaymentStatus($trackId, 'paid', $data); case 'underpaid': // Partial payment received return $this->updatePaymentStatus($trackId, 'underpaid', $data); case 'expired': // Payment expired return $this->updatePaymentStatus($trackId, 'expired', $data); case 'refunded': // Payment was refunded return $this->updatePaymentStatus($trackId, 'refunded', $data); case 'manual_accept': // Manually accepted return $this->updatePaymentStatus($trackId, 'manual_accept', $data); default: Log::warning('Unknown OxaPay webhook status', [ 'status' => $status, 'track_id' => $trackId, ]); return ['status' => 'unknown', 'message' => "Unknown status: {$status}"]; } } /** * Update payment status in local system */ protected function updatePaymentStatus(string $trackId, string $status, array $data): array { try { // Find subscription by provider subscription ID (track_id) $subscription = Subscription::where('provider', 'oxapay') ->where('provider_subscription_id', $trackId) ->first(); if (! $subscription) { Log::warning('OxaPay webhook: Subscription not found', [ 'track_id' => $trackId, 'status' => $status, ]); return [ 'status' => 'not_found', 'message' => 'Subscription not found for track_id: '.$trackId, ]; } // Update subscription based on payment status $updateData = [ 'provider_data' => array_merge($subscription->provider_data ?? [], $data), 'last_provider_sync' => now(), ]; // Map OxaPay statuses to Laravel subscription statuses switch ($status) { case 'paying': case 'underpaid': $updateData['status'] = 'pending_payment'; $updateData['unified_status'] = 'pending_payment'; break; case 'paid': case 'manual_accept': $updateData['status'] = 'active'; $updateData['unified_status'] = 'active'; $updateData['starts_at'] = now(); break; case 'refunded': $updateData['status'] = 'cancelled'; $updateData['unified_status'] = 'cancelled'; $updateData['cancelled_at'] = now(); break; case 'expired': $updateData['status'] = 'expired'; $updateData['unified_status'] = 'expired'; $updateData['ends_at'] = now(); break; } $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, 'status' => $status, 'new_status' => $updateData['status'], ]); return [ 'status' => 'updated', 'subscription_id' => $subscription->id, 'new_status' => $updateData['status'], ]; } catch (\Exception $e) { Log::error('OxaPay payment status update failed', [ 'track_id' => $trackId, 'status' => $status, 'error' => $e->getMessage(), ]); return [ 'status' => 'error', 'message' => $e->getMessage(), ]; } } public function getConfiguration(): array { return $this->config; } public function syncSubscriptionStatus(Subscription $subscription): array { try { $trackId = $subscription->provider_subscription_id; if (empty($trackId)) { throw new \Exception('No track_id found for subscription'); } Log::info('Syncing OxaPay subscription status', [ 'subscription_id' => $subscription->id, 'track_id' => $trackId, ]); $response = Http::withHeaders([ 'merchant_api_key' => $this->merchantApiKey, 'Content-Type' => 'application/json', ])->get("{$this->baseUrl}/payment/{$trackId}"); if (! $response->successful()) { throw new \Exception('Failed to fetch payment status from OxaPay'); } $data = $response->json(); if (! isset($data['data'])) { throw new \Exception('Invalid response from OxaPay API'); } $paymentData = $data['data']; $oxapayStatus = $paymentData['status'] ?? 'unknown'; // Map OxaPay status to Laravel subscription status $statusMapping = [ 'paying' => 'pending_payment', 'underpaid' => 'pending_payment', 'paid' => 'active', 'manual_accept' => 'active', 'expired' => 'expired', 'refunded' => 'cancelled', ]; $newStatus = $statusMapping[$oxapayStatus] ?? 'pending_payment'; // Update subscription with latest data $updateData = [ 'status' => $newStatus, 'unified_status' => $newStatus, 'provider_data' => array_merge($subscription->provider_data ?? [], $paymentData), 'last_provider_sync' => now(), ]; // Add timestamps based on status switch ($newStatus) { case 'active': if (! $subscription->starts_at) { $updateData['starts_at'] = now(); } break; case 'expired': $updateData['ends_at'] = now(); break; case 'cancelled': $updateData['cancelled_at'] = now(); break; } $subscription->update($updateData); Log::info('OxaPay subscription synced successfully', [ 'subscription_id' => $subscription->id, 'track_id' => $trackId, 'oxapay_status' => $oxapayStatus, 'new_status' => $newStatus, ]); return [ 'success' => true, 'status' => $newStatus, 'oxapay_status' => $oxapayStatus, 'provider_data' => $paymentData, ]; } catch (\Exception $e) { Log::error('OxaPay subscription sync failed', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'error' => $e->getMessage(), ]; } } /** * Calculate subscription end date based on plan billing cycle days */ protected function calculateSubscriptionEndDate(Plan $plan): ?Carbon { try { // Use billing_cycle_days from plans table $billingCycleDays = $plan->billing_cycle_days ?? 30; return now()->addDays($billingCycleDays); } catch (\Exception $e) { Log::error('Failed to calculate subscription end date', [ 'plan_id' => $plan->id, 'error' => $e->getMessage(), ]); // Safe fallback: 30 days return now()->addDays(30); } } public function getPaymentMethodDetails(string $paymentId): array { try { $response = Http::withHeaders([ 'merchant_api_key' => $this->merchantApiKey, ])->get("{$this->baseUrl}/payment/info", [ 'track_id' => $paymentId, ]); if ($response->successful()) { $data = $response->json(); return [ 'success' => true, 'details' => $data['data'] ?? [], ]; } return [ 'success' => false, 'error' => 'Payment not found', ]; } catch (\Exception $e) { return [ 'success' => false, 'error' => $e->getMessage(), ]; } } public function processRefund(string $paymentIntentId, float $amount, string $reason = ''): array { // OxaPay doesn't support traditional refunds in crypto // Would need manual payout process return [ 'success' => false, 'error' => 'OxaPay refunds must be processed manually via payouts', ]; } public function getTransactionHistory(User $user, array $filters = []): array { try { $response = Http::withHeaders([ 'merchant_api_key' => $this->merchantApiKey, ])->get("{$this->baseUrl}/payment/history", array_merge([ 'email' => $user->email, ], $filters)); if ($response->successful()) { $data = $response->json(); return [ 'success' => true, 'transactions' => $data['data'] ?? [], ]; } return [ 'success' => false, 'error' => 'Failed to fetch transaction history', ]; } catch (\Exception $e) { return [ 'success' => false, 'error' => $e->getMessage(), ]; } } public function getSubscriptionMetadata(Subscription $subscription): array { throw new \Exception('OxaPay does not support recurring subscriptions'); } public function updateSubscriptionMetadata(Subscription $subscription, array $metadata): bool { throw new \Exception('OxaPay does not support recurring subscriptions'); } public function startTrial(Subscription $subscription, int $trialDays): bool { throw new \Exception('OxaPay does not support recurring subscriptions'); } public function applyCoupon(Subscription $subscription, string $couponCode): array { throw new \Exception('OxaPay does not support recurring subscriptions'); } public function removeCoupon(Subscription $subscription): bool { throw new \Exception('OxaPay does not support recurring subscriptions'); } public function getUpcomingInvoice(Subscription $subscription): array { throw new \Exception('OxaPay does not support recurring subscriptions'); } public function retryFailedPayment(Subscription $subscription): array { throw new \Exception('OxaPay does not support recurring subscriptions'); } public function canModifySubscription(Subscription $subscription): bool { return false; // No recurring support } public function getCancellationTerms(Subscription $subscription): array { return [ 'immediate_cancellation' => true, 'refund_policy' => 'no_refunds_crypto', 'cancellation_effective' => 'immediately', 'billing_cycle_proration' => false, ]; } public function exportSubscriptionData(Subscription $subscription): array { throw new \Exception('OxaPay does not support recurring subscriptions'); } public function importSubscriptionData(User $user, array $subscriptionData): array { 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); } }