File "BulkNotificationsJobv3_0_0.php"
Full Path: /home/trinadezambia/public_html/admin_panel/app/Jobs/BulkNotificationsJobv3_0_0.php
File size: 9.85 KB
MIME-type: text/x-php
Charset: utf-8
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\FcmToken;
use App\Models\School;
use App\Models\User;
use App\Services\CachingService;
use App\Services\FcmTokenService;
use Google\Client;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Client\Pool;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;
final class BulkNotificationsJobv3_0_0 implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public int $tries = 3;
public int $timeout = 120;
public int $backoff = 30;
/**
* @param int $schoolId
* @param array $userIds
* @param string $title
* @param string $body
* @param string $type
* @param array{
* student_map?: array<int,int>,
* guardian_map?: array<int,array<int>|int>,
* image?: string
* } $customData
*/
public function __construct(
private readonly int $schoolId,
private readonly array $userIds,
private readonly string $title,
private readonly string $body,
private readonly string $type,
private readonly array $customData = []
) {}
public function handle(
CachingService $cache,
FcmTokenService $tokenService
): void {
DB::setDefaultConnection('mysql');
try {
$school = School::on('mysql')->find($this->schoolId);
if (!$school)
return;
Config::set('database.connections.school.database', $school->database_name);
DB::purge('school');
DB::connection('school')->reconnect();
DB::setDefaultConnection('school');
$internal = $this->customData['internal'] ?? [];
$payloadBase = array_map(
'strval',
$this->customData['payload'] ?? []
);
$tokens = $tokenService->getUsersTokens($this->userIds);
if ($tokens->isEmpty()) {
foreach ($this->userIds as $userId) {
$user = User::find($userId);
if ($user) {
$tokenService->migrateUserTokens($user);
}
}
$tokens = $tokenService->getUsersTokens($this->userIds);
}
if ($tokens->isEmpty())
return;
$targets = $this->resolveTargets($tokens, $internal);
if (empty($targets))
return;
$accessToken = $this->getAccessToken($cache);
if (!$accessToken)
return;
$url = "https://fcm.googleapis.com/v1/projects/{$cache->getSystemSettings('firebase_project_id')}/messages:send";
$sectionMap = $internal['section_map'] ?? [];
Http::pool(function ($pool) use ($targets, $payloadBase, $accessToken, $url, $tokenService, $sectionMap) {
foreach ($targets as $target) {
$token = $target['token'];
$childId = $target['child_id'];
$classSubjectId = null;
if ($childId) {
// child_id == student_id
$student = \App\Models\Students::find($childId, ['class_section_id']);
if ($student && isset($sectionMap[$student->class_section_id])) {
$classSubjectId = $sectionMap[$student->class_section_id];
}
}
$data = array_merge([
'title' => $this->title,
'body' => $this->body,
'type' => $this->type,
], $payloadBase);
if ($childId) {
$data['child_id'] = (string) $childId;
}
if ($classSubjectId) {
$data['class_subject_id'] = (string) $classSubjectId;
}
// Log::info('Sending FCM notification', [
// 'data' => $data
// ]);
$payload = [
'message' => [
'token' => $token->fcm_token,
'data' => $data,
'notification' => [
'title' => $this->title,
'body' => $this->body,
],
'android' => [
'priority' => 'high',
'notification' => [
'sound' => 'default',
'click_action' => 'FLUTTER_NOTIFICATION_CLICK',
'tag' => 'announcement_' . ($childId ?? uniqid()),
],
],
],
];
// Add web-specific configuration for web tokens
if ($token->device_type === 'web') {
$iconUrl = $this->getWebNotificationIcon();
$webNotification = [
'title' => $this->title,
'body' => $this->body,
'icon' => $iconUrl,
'badge' => $iconUrl,
'requireInteraction' => false,
'silent' => false,
];
if (isset($this->customData['image']) && !empty($this->customData['image'])) {
$imageUrl = $this->customData['image'];
if (!filter_var($imageUrl, FILTER_VALIDATE_URL)) {
$imageUrl = url($imageUrl);
}
$webNotification['image'] = $imageUrl;
}
$payload['message']['webpush'] = [
'notification' => $webNotification,
];
}
$pool
->withToken($accessToken)
->post($url, $payload)
->then(fn($r) => $this->handleResponse($r->json(), $token, $tokenService));
}
});
} finally {
DB::setDefaultConnection('mysql');
}
}
/* ================= CORE FIX ================= */
private function resolveTargets($tokens, array $internal): array
{
$targets = [];
$studentMap = $internal['student_map'] ?? [];
$guardianMap = $internal['guardian_map'] ?? [];
foreach ($tokens as $token) {
$userId = $token->user_id;
if (isset($guardianMap[$userId])) {
foreach ($guardianMap[$userId] as $childId) {
$targets[] = [
'token' => $token,
'child_id' => $childId,
];
}
continue;
}
if (isset($studentMap[$userId])) {
$targets[] = [
'token' => $token,
'child_id' => $studentMap[$userId],
];
continue;
}
$targets[] = [
'token' => $token,
'child_id' => null,
];
}
return $targets;
}
/* ================= Helpers ================= */
private function handleResponse(?array $response, FcmToken $token, FcmTokenService $service): void
{
if (!$response)
return;
if (isset($response['error'])) {
$status = $response['error']['status'] ?? null;
if (in_array($status, ['NOT_FOUND', 'UNREGISTERED'], true)) {
$service->removeInvalidToken($token->fcm_token);
return;
}
// Payload / request errors → log only
Log::warning('FCM send failed', [
'status' => $status,
'message' => $response['error']['message'] ?? null,
'token_id' => $token->id,
]);
return;
}
$token->touchLastUsed();
}
private function getAccessToken(CachingService $cache): ?string
{
try {
$fileName = $cache->getSystemSettings('firebase_service_file');
$data = explode('storage/', $fileName ?? '');
$fileName = end($data);
$filePath = base_path('public/storage/' . $fileName);
if (!file_exists($filePath)) {
Log::error('Firebase service file not found', ['path' => $filePath]);
return null;
}
$client = new Client();
$client->setAuthConfig($filePath);
$client->setScopes(['https://www.googleapis.com/auth/firebase.messaging']);
$accessToken = $client->fetchAccessTokenWithAssertion()['access_token'] ?? null;
return $accessToken;
} catch (Throwable $e) {
Log::error('Failed to get Firebase access token', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return null;
}
}
private function getWebNotificationIcon(): string
{
$defaultIcon = asset('assets/images/favicon.png');
if (isset($this->customData['image']) && !empty($this->customData['image'])) {
$image = $this->customData['image'];
if (filter_var($image, FILTER_VALIDATE_URL)) {
return $image;
}
return url($image);
}
return $defaultIcon;
}
}