From 045b8d1f4c02dff9d51234e2e49331381a44123b Mon Sep 17 00:00:00 2001 From: mohanmd Date: Sun, 14 Jun 2026 17:27:54 +0530 Subject: [PATCH] push notification api --- .../Admin/AdminExtraController.php | 77 ++++++++++++++++++- .../Api/EmployerAuthController.php | 7 +- .../Api/EmployerProfileController.php | 9 ++- .../Api/EmployerWorkerController.php | 13 ++++ .../Controllers/Api/WorkerAuthController.php | 7 +- .../Api/WorkerMessageController.php | 15 ++++ .../Api/WorkerProfileController.php | 16 ++++ app/Models/User.php | 2 +- app/Services/FCMService.php | 14 ++++ ...14_173000_add_fcm_token_to_users_table.php | 28 +++++++ tests/Feature/WorkerMessageApiTest.php | 36 +++++++++ 11 files changed, 218 insertions(+), 6 deletions(-) create mode 100644 database/migrations/2026_06_14_173000_add_fcm_token_to_users_table.php diff --git a/app/Http/Controllers/Admin/AdminExtraController.php b/app/Http/Controllers/Admin/AdminExtraController.php index e40b92a..611a946 100644 --- a/app/Http/Controllers/Admin/AdminExtraController.php +++ b/app/Http/Controllers/Admin/AdminExtraController.php @@ -296,6 +296,33 @@ public function broadcastNotification(Request $request) 'updated_at' => now(), ]); + // Send FCM push notifications for broadcast + if ($request->channel === 'push' || $request->channel === 'both') { + $recipientsLower = strtolower($request->recipients); + if (str_contains($recipientsLower, 'worker') || $recipientsLower === 'all') { + $workers = \App\Models\Worker::whereNotNull('fcm_token')->get(); + foreach ($workers as $worker) { + \App\Services\FCMService::sendPushNotification( + $worker->fcm_token, + $request->title, + $request->message, + ['type' => 'broadcast'] + ); + } + } + if (str_contains($recipientsLower, 'employer') || $recipientsLower === 'all') { + $employers = \App\Models\User::where('role', 'employer')->whereNotNull('fcm_token')->get(); + foreach ($employers as $employer) { + \App\Services\FCMService::sendPushNotification( + $employer->fcm_token, + $request->title, + $request->message, + ['type' => 'broadcast'] + ); + } + } + } + return back()->with('success', "Notification campaign broadcasted successfully to all {$request->recipients} users!"); } @@ -523,13 +550,15 @@ public function storeAnnouncement(Request $request) 'content' => $request->content, ]); - \App\Models\Announcement::create([ + $ann = \App\Models\Announcement::create([ 'title' => $request->title, 'body' => $body, 'type' => 'Charity', 'status' => 'approved', ]); + $this->notifyUsersOfEvent($ann); + return back()->with('success', 'Charity Event created successfully.'); } @@ -541,6 +570,8 @@ public function approveAnnouncement(Request $request, $id) $ann = \App\Models\Announcement::findOrFail($id); $ann->update(['status' => 'approved']); + $this->notifyUsersOfEvent($ann); + return back()->with('success', 'Charity Event approved successfully.'); } @@ -572,4 +603,48 @@ public function deleteAnnouncement(Request $request, $id) return back()->with('success', 'Charity Event deleted successfully.'); } + + /** + * Broadcast FCM Push Notification for new Event/Announcement. + */ + protected function notifyUsersOfEvent($ann) + { + $title = "New Event: " . $ann->title; + $body = $ann->body; + + if (str_starts_with($ann->body, '{"type":"Charity"')) { + $decoded = json_decode($ann->body, true); + if ($decoded && isset($decoded['content'])) { + $body = $decoded['content']; + } + } + + // Notify all workers + $workers = \App\Models\Worker::whereNotNull('fcm_token')->get(); + foreach ($workers as $worker) { + \App\Services\FCMService::sendPushNotification( + $worker->fcm_token, + $title, + $body, + [ + 'type' => 'announcement', + 'announcement_id' => $ann->id, + ] + ); + } + + // Notify all employers + $employers = \App\Models\User::where('role', 'employer')->whereNotNull('fcm_token')->get(); + foreach ($employers as $employer) { + \App\Services\FCMService::sendPushNotification( + $employer->fcm_token, + $title, + $body, + [ + 'type' => 'announcement', + 'announcement_id' => $ann->id, + ] + ); + } + } } diff --git a/app/Http/Controllers/Api/EmployerAuthController.php b/app/Http/Controllers/Api/EmployerAuthController.php index be54373..22a0768 100644 --- a/app/Http/Controllers/Api/EmployerAuthController.php +++ b/app/Http/Controllers/Api/EmployerAuthController.php @@ -25,6 +25,7 @@ public function login(Request $request) 'email' => 'nullable|string', 'mobile' => 'nullable|string', 'password' => 'required|string', + 'fcm_token' => 'nullable|string|max:255', ]); $validator->after(function ($validator) use ($request) { @@ -103,7 +104,11 @@ public function login(Request $request) ], 403); } - $user->update(['api_token' => $apiToken]); + $userUpdateData = ['api_token' => $apiToken]; + if ($request->has('fcm_token')) { + $userUpdateData['fcm_token'] = $request->fcm_token; + } + $user->update($userUpdateData); if ($sponsor) { $sponsor->update([ 'api_token' => $apiToken, diff --git a/app/Http/Controllers/Api/EmployerProfileController.php b/app/Http/Controllers/Api/EmployerProfileController.php index 4c0e692..8a7e567 100644 --- a/app/Http/Controllers/Api/EmployerProfileController.php +++ b/app/Http/Controllers/Api/EmployerProfileController.php @@ -93,6 +93,7 @@ public function updateProfile(Request $request) 'notifications' => 'required|boolean', 'current_password' => 'nullable|required_with:new_password|string', 'new_password' => 'nullable|string|min:8|confirmed', + 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { @@ -121,10 +122,14 @@ public function updateProfile(Request $request) $oldEmail = $employer->getOriginal('email') ?? $employer->email; // Update user table - $employer->update([ + $userData = [ 'name' => $request->name, 'email' => $request->email, - ]); + ]; + if ($request->has('fcm_token')) { + $userData['fcm_token'] = $request->fcm_token; + } + $employer->update($userData); // Sync with corresponding sponsor record if found $matchingSponsor = \App\Models\Sponsor::where('email', $oldEmail)->first(); diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php index 7c90f90..8bd69cf 100644 --- a/app/Http/Controllers/Api/EmployerWorkerController.php +++ b/app/Http/Controllers/Api/EmployerWorkerController.php @@ -798,6 +798,19 @@ public function hireCandidate(Request $request, $id = null) } } + // Dispatch push notification to worker + if ($worker && $worker->fcm_token) { + \App\Services\FCMService::sendPushNotification( + $worker->fcm_token, + "Congratulations! You've been Hired", + "Employer " . ($employer->name ?? "Employer") . " has hired you.", + [ + 'type' => 'hired', + 'employer_id' => $employer->id, + ] + ); + } + return response()->json([ 'success' => true, 'message' => 'Candidate successfully marked as Hired!', diff --git a/app/Http/Controllers/Api/WorkerAuthController.php b/app/Http/Controllers/Api/WorkerAuthController.php index 6490cab..569b870 100644 --- a/app/Http/Controllers/Api/WorkerAuthController.php +++ b/app/Http/Controllers/Api/WorkerAuthController.php @@ -441,6 +441,7 @@ public function login(Request $request) 'phone' => 'required_without:email|nullable|string', 'email' => 'required_without:phone|nullable|email', 'password' => 'required|string', + 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { @@ -464,7 +465,11 @@ public function login(Request $request) } $apiToken = Str::random(80); - $worker->update(['api_token' => $apiToken]); + $updateData = ['api_token' => $apiToken]; + if ($request->has('fcm_token')) { + $updateData['fcm_token'] = $request->fcm_token; + } + $worker->update($updateData); return response()->json([ 'success' => true, diff --git a/app/Http/Controllers/Api/WorkerMessageController.php b/app/Http/Controllers/Api/WorkerMessageController.php index 8a61984..a13a57d 100644 --- a/app/Http/Controllers/Api/WorkerMessageController.php +++ b/app/Http/Controllers/Api/WorkerMessageController.php @@ -271,6 +271,21 @@ public function sendMessage(Request $request, $id) } }); + // Dispatch push notification to employer + $employer = $conv->employer; + if ($employer && $employer->fcm_token) { + \App\Services\FCMService::sendPushNotification( + $employer->fcm_token, + "New Message from " . ($worker->name ?? "Worker"), + $request->text ?: "Sent an attachment", + [ + 'conversation_id' => $conv->id, + 'sender_type' => 'worker', + 'sender_id' => $worker->id, + ] + ); + } + return response()->json([ 'success' => true, 'message' => 'Message sent successfully.', diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index b1c3341..09ca911 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -351,6 +351,22 @@ public function respondToOffer(Request $request, $id) } }); + // Dispatch push notification to employer + $employer = $offer->employer; + if ($employer && $employer->fcm_token) { + $statusMessage = $responseStatus === 'accepted' ? 'accepted' : 'rejected'; + \App\Services\FCMService::sendPushNotification( + $employer->fcm_token, + "Job Offer " . ucfirst($statusMessage), + "Worker " . ($worker->name ?? "Candidate") . " has " . $statusMessage . " your job offer.", + [ + 'type' => 'job_offer_response', + 'offer_id' => $offer->id, + 'status' => $responseStatus, + ] + ); + } + return response()->json([ 'success' => true, 'message' => "Job offer successfully " . $responseStatus . ".", diff --git a/app/Models/User.php b/app/Models/User.php index c6b81d3..5b9e6e9 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,7 +10,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -#[Fillable(['name', 'email', 'password', 'role', 'status', 'subscription_status', 'subscription_expires_at', 'api_token'])] +#[Fillable(['name', 'email', 'password', 'role', 'status', 'subscription_status', 'subscription_expires_at', 'api_token', 'fcm_token'])] #[Hidden(['password', 'remember_token', 'api_token'])] class User extends Authenticatable { diff --git a/app/Services/FCMService.php b/app/Services/FCMService.php index 29706e0..f6d3c00 100644 --- a/app/Services/FCMService.php +++ b/app/Services/FCMService.php @@ -119,6 +119,20 @@ public static function sendPushNotification($token, $title, $body, array $data = 'title' => $title, 'body' => $body, ], + 'android' => [ + 'notification' => [ + 'click_action' => 'FLUTTER_NOTIFICATION_CLICK', + 'sound' => 'default', + ], + ], + 'apns' => [ + 'payload' => [ + 'aps' => [ + 'sound' => 'default', + 'badge' => 1, + ], + ], + ], ] ]; diff --git a/database/migrations/2026_06_14_173000_add_fcm_token_to_users_table.php b/database/migrations/2026_06_14_173000_add_fcm_token_to_users_table.php new file mode 100644 index 0000000..1ff9d43 --- /dev/null +++ b/database/migrations/2026_06_14_173000_add_fcm_token_to_users_table.php @@ -0,0 +1,28 @@ +string('fcm_token')->nullable()->after('api_token'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('fcm_token'); + }); + } +}; diff --git a/tests/Feature/WorkerMessageApiTest.php b/tests/Feature/WorkerMessageApiTest.php index 0cad9dd..9e2d04f 100644 --- a/tests/Feature/WorkerMessageApiTest.php +++ b/tests/Feature/WorkerMessageApiTest.php @@ -202,4 +202,40 @@ public function test_unauthenticated_requests_are_blocked() $response = $this->getJson('/api/workers/conversations'); $response->assertStatus(401); } + + /** + * Test worker message triggers push notification. + */ + public function test_worker_message_triggers_push_notification() + { + \Illuminate\Support\Facades\Http::fake([ + 'https://oauth2.googleapis.com/token' => \Illuminate\Support\Facades\Http::response(['access_token' => 'mocked-fcm-token'], 200), + 'https://fcm.googleapis.com/*' => \Illuminate\Support\Facades\Http::response(['name' => 'projects/migrant-fe5a7/messages/mock-id'], 200), + ]); + + // Set fcm_token on employer + $this->employer->update(['fcm_token' => 'employer-device-fcm-token-123']); + + // Create conversation + $conversation = Conversation::create([ + 'employer_id' => $this->employer->id, + 'worker_id' => $this->worker->id, + ]); + + // Reply request + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->postJson('/api/workers/conversations/' . $conversation->id . '/messages', [ + 'text' => 'Testing push notification to employer!' + ]); + + $response->assertStatus(201); + + // Assert Http call was made to FCM endpoint + \Illuminate\Support\Facades\Http::assertSent(function ($request) { + return str_contains($request->url(), 'fcm.googleapis.com') && + $request['message']['token'] === 'employer-device-fcm-token-123' && + $request['message']['notification']['body'] === 'Testing push notification to employer!'; + }); + } }