From 52b47cc2ace0fba465321dc00fedcc4320467a74 Mon Sep 17 00:00:00 2001 From: mohanmd Date: Sat, 4 Jul 2026 21:39:46 +0530 Subject: [PATCH] changes on api worker employer details, review, issue --- .../Api/WorkerProfileController.php | 151 ++++++++++++++++ .../Api/WorkerReviewController.php | 6 + app/Http/Middleware/EmployerMiddleware.php | 2 + public/swagger.json | 155 ++++++++++++++++ routes/api.php | 1 + tests/Feature/EmployerProfileWebTest.php | 27 +++ tests/Feature/WorkerEmployersApiTest.php | 168 ++++++++++++++++++ tests/Feature/WorkerReviewTest.php | 33 ++++ 8 files changed, 543 insertions(+) diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index 688e956..9f5664c 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -933,4 +933,155 @@ private function cleanVisaData(array $visaDataInput): array ] ]; } + + /** + * Get details of an employer profile for worker view. + * GET /api/workers/employers/{id} + */ + public function getEmployerProfile(Request $request, $id) + { + /** @var Worker $worker */ + $worker = $request->attributes->get('worker'); + + try { + // Find the employer (role should be 'employer') + $employer = \App\Models\User::where('id', $id)->where('role', 'employer')->first(); + + if (!$employer) { + return response()->json([ + 'success' => false, + 'message' => 'Employer not found.' + ], 404); + } + + $empProfile = $employer->employerProfile; + + // 1. Check access permissions for contact details + $hasAccess = false; + + // Check if there is an active/previous JobOffer + $hasOffer = \App\Models\JobOffer::where('employer_id', $employer->id) + ->where('worker_id', $worker->id) + ->exists(); + + // Check if the worker has applied to any jobs of this employer + $hasApplication = \App\Models\JobApplication::where('worker_id', $worker->id) + ->whereHas('jobPost', function ($query) use ($employer) { + $query->where('employer_id', $employer->id); + }) + ->exists(); + + // Check if there is an active/previous conversation + $hasConversation = \App\Models\Conversation::where('employer_id', $employer->id) + ->where('worker_id', $worker->id) + ->exists(); + + if ($hasOffer || $hasApplication || $hasConversation) { + $hasAccess = true; + } + + // 2. Fetch active job details (if applicable) + $activeJobs = \App\Models\JobPost::where('employer_id', $employer->id) + ->where('status', 'active') + ->latest() + ->get() + ->map(function ($job) { + return [ + 'id' => $job->id, + 'title' => $job->title, + 'location' => $job->location, + 'salary' => (int) $job->salary, + 'job_type' => $job->job_type, + 'start_date' => $job->start_date ? $job->start_date->format('Y-m-d') : null, + 'description' => $job->description, + 'requirements' => $job->requirements, + 'posted_at' => $job->created_at->toIso8601String(), + ]; + }); + + // 3. Overall Rating and Review Summary + $reviewsQuery = \App\Models\EmployerReview::where('employer_id', $employer->id); + $totalReviews = $reviewsQuery->count(); + $avgRating = $totalReviews > 0 ? round($reviewsQuery->avg('rating'), 1) : 0.0; + + $starsBreakdown = [ + '5' => \App\Models\EmployerReview::where('employer_id', $employer->id)->where('rating', 5)->count(), + '4' => \App\Models\EmployerReview::where('employer_id', $employer->id)->where('rating', 4)->count(), + '3' => \App\Models\EmployerReview::where('employer_id', $employer->id)->where('rating', 3)->count(), + '2' => \App\Models\EmployerReview::where('employer_id', $employer->id)->where('rating', 2)->count(), + '1' => \App\Models\EmployerReview::where('employer_id', $employer->id)->where('rating', 1)->count(), + ]; + + // 4. Paginated Reviews list + $perPage = (int) $request->input('per_page', 10); + $reviewsPaginator = \App\Models\EmployerReview::where('employer_id', $employer->id) + ->with('worker') + ->latest() + ->paginate($perPage); + + $reviews = collect($reviewsPaginator->items())->map(function ($rev) { + return [ + 'id' => $rev->id, + 'rating' => $rev->rating, + 'title' => $rev->title, + 'comment' => $rev->comment, + 'created_at' => $rev->created_at->toIso8601String(), + 'worker' => $rev->worker ? [ + 'id' => $rev->worker->id, + 'name' => $rev->worker->name, + 'nationality' => $rev->worker->nationality, + ] : null, + ]; + }); + + // 5. Structure the response + $employerData = [ + 'id' => $employer->id, + 'name' => $employer->name, + 'email' => $hasAccess ? $employer->email : null, + 'phone' => $hasAccess ? ($empProfile->phone ?? $employer->phone) : null, + 'company_name' => $empProfile->company_name ?? 'Private Sponsor', + 'city' => $empProfile->city ?? null, + 'district' => $empProfile->district ?? null, + 'nationality' => $empProfile->nationality ?? null, + 'address' => $empProfile->address ?? null, + 'accommodation' => $empProfile->accommodation ?? null, + 'language' => $empProfile->language ?? null, + 'profile_photo_url' => $empProfile->profile_photo_url ?? null, + 'rating' => $avgRating, + 'reviews_count' => $totalReviews, + 'review_summary' => [ + 'total' => $totalReviews, + 'average' => $avgRating, + 'stars' => $starsBreakdown, + ], + 'active_jobs' => $activeJobs, + ]; + + return response()->json([ + 'success' => true, + 'data' => [ + 'employer' => $employerData, + 'reviews' => [ + 'data' => $reviews, + 'pagination' => [ + 'total' => $reviewsPaginator->total(), + 'per_page' => $reviewsPaginator->perPage(), + 'current_page' => $reviewsPaginator->currentPage(), + 'last_page' => $reviewsPaginator->lastPage(), + ] + ] + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Worker View Employer Profile Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while loading the employer profile.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } } diff --git a/app/Http/Controllers/Api/WorkerReviewController.php b/app/Http/Controllers/Api/WorkerReviewController.php index 590551b..9d6eda9 100644 --- a/app/Http/Controllers/Api/WorkerReviewController.php +++ b/app/Http/Controllers/Api/WorkerReviewController.php @@ -24,6 +24,12 @@ public function addReview(Request $request) /** @var Worker $worker */ $worker = $request->attributes->get('worker'); + // Sanitize job_id: if not a valid existing JobPost ID, treat it as null (job id is not needed/optional) + $jobIdInput = $request->input('job_id'); + if (empty($jobIdInput) || !is_numeric($jobIdInput) || !\App\Models\JobPost::where('id', $jobIdInput)->exists()) { + $request->merge(['job_id' => null]); + } + $validator = Validator::make($request->all(), [ 'employer_id' => 'required|exists:users,id', 'job_id' => 'nullable|exists:job_posts,id', diff --git a/app/Http/Middleware/EmployerMiddleware.php b/app/Http/Middleware/EmployerMiddleware.php index 62ba7e2..308ac49 100644 --- a/app/Http/Middleware/EmployerMiddleware.php +++ b/app/Http/Middleware/EmployerMiddleware.php @@ -33,6 +33,8 @@ public function handle(Request $request, Closure $next): Response } if (!$user) { + session()->forget('user'); + auth()->logout(); return redirect()->route('employer.login'); } diff --git a/public/swagger.json b/public/swagger.json index 6c53ef7..bb228af 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -7305,6 +7305,161 @@ } } }, + "/workers/employers/{id}": { + "get": { + "tags": [ + "Worker/Employers" + ], + "summary": "Worker View Employer Profile", + "description": "Allows authenticated workers to view the complete profile of a specific employer, including company details, ratings, review summaries, active jobs, and a paginated list of worker-submitted reviews. Contact details are conditionally visible depending on whether the worker has a past/present connection with the employer.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The unique database ID of the employer.", + "schema": { + "type": "integer", + "example": 1 + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "description": "Number of reviews to display per page (default: 10).", + "schema": { + "type": "integer", + "example": 10 + } + }, + { + "name": "page", + "in": "query", + "required": false, + "description": "The page number of the reviews list to fetch.", + "schema": { + "type": "integer", + "example": 1 + } + } + ], + "responses": { + "200": { + "description": "Employer profile loaded successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "employer": { + "type": "object", + "properties": { + "id": { "type": "integer", "example": 1 }, + "name": { "type": "string", "example": "Jane Sponsor" }, + "email": { "type": "string", "nullable": true, "example": "sponsor@example.com" }, + "phone": { "type": "string", "nullable": true, "example": "+971501112222" }, + "company_name": { "type": "string", "example": "Sponsor Co" }, + "city": { "type": "string", "nullable": true, "example": "Dubai" }, + "district": { "type": "string", "nullable": true, "example": "Dubai Marina" }, + "nationality": { "type": "string", "nullable": true, "example": "Emirati" }, + "address": { "type": "string", "nullable": true, "example": "123 Street" }, + "accommodation": { "type": "string", "nullable": true, "example": "Provided" }, + "language": { "type": "string", "nullable": true, "example": "English" }, + "profile_photo_url": { "type": "string", "nullable": true, "example": "https://example.com/photo.jpg" }, + "rating": { "type": "number", "format": "float", "example": 4.5 }, + "reviews_count": { "type": "integer", "example": 12 }, + "review_summary": { + "type": "object", + "properties": { + "total": { "type": "integer", "example": 12 }, + "average": { "type": "number", "example": 4.5 }, + "stars": { + "type": "object", + "properties": { + "5": { "type": "integer", "example": 8 }, + "4": { "type": "integer", "example": 2 }, + "3": { "type": "integer", "example": 1 }, + "2": { "type": "integer", "example": 1 }, + "1": { "type": "integer", "example": 0 } + } + } + } + }, + "active_jobs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "integer", "example": 5 }, + "title": { "type": "string", "example": "Housekeeper Needed" }, + "location": { "type": "string", "example": "Dubai Marina" }, + "salary": { "type": "integer", "example": 2500 }, + "job_type": { "type": "string", "example": "Full Time" }, + "start_date": { "type": "string", "format": "date", "example": "2026-08-01" }, + "description": { "type": "string", "example": "Clean house" }, + "requirements": { "type": "string", "nullable": true, "example": "Experience required" }, + "posted_at": { "type": "string", "format": "date-time", "example": "2026-07-04T12:00:00Z" } + } + } + } + } + }, + "reviews": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "integer", "example": 1 }, + "rating": { "type": "integer", "example": 5 }, + "title": { "type": "string", "example": "Great employer" }, + "comment": { "type": "string", "example": "Highly recommended!" }, + "created_at": { "type": "string", "format": "date-time", "example": "2026-07-04T12:00:00Z" }, + "worker": { + "type": "object", + "properties": { + "id": { "type": "integer", "example": 2 }, + "name": { "type": "string", "example": "Worker Name" }, + "nationality": { "type": "string", "example": "Filipino" } + } + } + } + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { "type": "integer", "example": 1 }, + "per_page": { "type": "integer", "example": 10 }, + "current_page": { "type": "integer", "example": 1 }, + "last_page": { "type": "integer", "example": 1 } + } + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Employer not found." + } + } + } + }, "/workers/forgot-password": { "post": { "tags": [ diff --git a/routes/api.php b/routes/api.php index 42609b9..3bd494d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -78,6 +78,7 @@ Route::get('/workers/dashboard/views', [WorkerProfileController::class, 'getProfileViews']); Route::get('/workers/dashboard', [WorkerProfileController::class, 'getDashboard']); Route::get('/workers/employers', [WorkerProfileController::class, 'getEmployers']); + Route::get('/workers/employers/{id}', [WorkerProfileController::class, 'getEmployerProfile']); Route::post('/workers/change-password', [WorkerProfileController::class, 'changePassword']); diff --git a/tests/Feature/EmployerProfileWebTest.php b/tests/Feature/EmployerProfileWebTest.php index de46962..ac8d788 100644 --- a/tests/Feature/EmployerProfileWebTest.php +++ b/tests/Feature/EmployerProfileWebTest.php @@ -85,4 +85,31 @@ public function test_employer_upload_emirates_id_ocr_and_purge() $this->assertNotNull($profile->emirates_id_expiry); $this->assertEquals('approved', $profile->verification_status); } + + /** + * Test that a stale session with an invalid user ID does not cause a redirect loop. + */ + public function test_stale_session_does_not_cause_redirect_loop() + { + // 1. Visit with a session user object containing a non-existent ID + $staleUser = (object)[ + 'id' => 999999, // Doesn't exist + 'name' => 'Stale User', + 'email' => 'stale@example.com', + 'role' => 'employer', + 'subscription_status' => 'active', + 'verification_status' => 'approved', + ]; + + // Accessing dashboard should fail auth, clear session, and redirect to login + $response = $this->withSession(['user' => $staleUser]) + ->get('/employer/dashboard'); + + $response->assertRedirect(route('employer.login')); + $this->assertNull(session('user')); // Session must be cleared + + // Subsequent access to login page should render successfully (no redirect) + $loginResponse = $this->get('/employer/login'); + $loginResponse->assertStatus(200); + } } diff --git a/tests/Feature/WorkerEmployersApiTest.php b/tests/Feature/WorkerEmployersApiTest.php index 69303b4..85b130e 100644 --- a/tests/Feature/WorkerEmployersApiTest.php +++ b/tests/Feature/WorkerEmployersApiTest.php @@ -236,4 +236,172 @@ public function test_worker_employers_list_pagination_and_sorting() $this->assertEquals(3, $responsePaginated->json('data.pagination.total')); $this->assertEquals(2, $responsePaginated->json('data.pagination.per_page')); } + + public function test_worker_can_view_employer_profile_with_permissions_and_reviews() + { + // Create worker + $worker = Worker::create([ + 'name' => 'Rahul Sharma', + 'email' => 'rahul@example.com', + 'phone' => '+971501234567', + 'language' => 'HI', + 'password' => bcrypt('password'), + 'nationality' => 'Indian', + 'age' => 25, + 'salary' => 1500, + 'availability' => 'Immediate', + 'experience' => 'Not Specified', + 'religion' => 'Not Specified', + 'bio' => 'Test', + 'verified' => true, + 'status' => 'active', + 'api_token' => 'worker-test-token', + ]); + + WorkerDocument::create([ + 'worker_id' => $worker->id, + 'type' => 'passport', + 'number' => '123456', + 'file_path' => 'passports/test.pdf', + ]); + + // Create an employer + $employer = User::create([ + 'name' => 'Employer One', + 'email' => 'emp1@example.com', + 'password' => bcrypt('password'), + 'role' => 'employer', + ]); + + EmployerProfile::create([ + 'user_id' => $employer->id, + 'company_name' => 'Ahmad Tech Ltd', + 'phone' => '+971500000001', + 'nationality' => 'UAE', + 'city' => 'Dubai', + 'accommodation' => 'Provided', + 'language' => 'English', + ]); + + // Create active job post + \App\Models\JobPost::create([ + 'employer_id' => $employer->id, + 'title' => 'Housekeeper Needed', + 'location' => 'Dubai Marina', + 'salary' => 2500, + 'workers_needed' => 1, + 'job_type' => 'Full Time', + 'start_date' => now()->addDays(5)->toDateString(), + 'description' => 'Clean house', + 'status' => 'active', + ]); + + // Create an accepted job offer (connection exists) + JobOffer::create([ + 'employer_id' => $employer->id, + 'worker_id' => $worker->id, + 'work_date' => now()->subDays(10), + 'location' => 'Dubai', + 'salary' => 2500, + 'status' => 'accepted', + ]); + + // Create a review submitted by another worker + $otherWorker = Worker::create([ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'phone' => '+971501234568', + 'password' => bcrypt('password'), + 'nationality' => 'Filipino', + 'age' => 25, + 'salary' => 1500, + 'availability' => 'Immediate', + 'experience' => 'Not Specified', + 'religion' => 'Not Specified', + 'bio' => 'Test', + 'passport_status' => 'valid', + ]); + + \App\Models\EmployerReview::create([ + 'worker_id' => $otherWorker->id, + 'employer_id' => $employer->id, + 'rating' => 5, + 'title' => 'Excellent sponsor', + 'comment' => 'Very polite and pays on time.', + ]); + + // Hit the new API endpoint + $response = $this->withHeaders([ + 'Authorization' => 'Bearer worker-test-token', + ])->getJson("/api/workers/employers/{$employer->id}"); + + $response->assertStatus(200); + $response->assertJsonPath('success', true); + $response->assertJsonPath('data.employer.name', 'Employer One'); + $response->assertJsonPath('data.employer.email', 'emp1@example.com'); // Contact info visible because of JobOffer connection + $response->assertJsonPath('data.employer.phone', '+971500000001'); + $response->assertJsonPath('data.employer.company_name', 'Ahmad Tech Ltd'); + $response->assertJsonPath('data.employer.rating', 5); + $response->assertJsonPath('data.employer.reviews_count', 1); + $response->assertJsonPath('data.employer.review_summary.stars.5', 1); + $response->assertJsonCount(1, 'data.employer.active_jobs'); + $response->assertJsonPath('data.employer.active_jobs.0.title', 'Housekeeper Needed'); + $response->assertJsonCount(1, 'data.reviews.data'); + $response->assertJsonPath('data.reviews.data.0.worker.name', 'John Doe'); + $response->assertJsonPath('data.reviews.data.0.title', 'Excellent sponsor'); + } + + public function test_worker_can_view_employer_profile_without_permissions_masks_contact() + { + // Create worker + $worker = Worker::create([ + 'name' => 'Rahul Sharma', + 'email' => 'rahul@example.com', + 'phone' => '+971501234567', + 'language' => 'HI', + 'password' => bcrypt('password'), + 'nationality' => 'Indian', + 'age' => 25, + 'salary' => 1500, + 'availability' => 'Immediate', + 'experience' => 'Not Specified', + 'religion' => 'Not Specified', + 'bio' => 'Test', + 'verified' => true, + 'status' => 'active', + 'api_token' => 'worker-test-token', + ]); + + WorkerDocument::create([ + 'worker_id' => $worker->id, + 'type' => 'passport', + 'number' => '123456', + 'file_path' => 'passports/test.pdf', + ]); + + // Create an employer + $employer = User::create([ + 'name' => 'Employer One', + 'email' => 'emp1@example.com', + 'password' => bcrypt('password'), + 'role' => 'employer', + ]); + + EmployerProfile::create([ + 'user_id' => $employer->id, + 'company_name' => 'Ahmad Tech Ltd', + 'phone' => '+971500000001', + ]); + + // Hit the API (no connection exists) + $response = $this->withHeaders([ + 'Authorization' => 'Bearer worker-test-token', + ])->getJson("/api/workers/employers/{$employer->id}"); + + $response->assertStatus(200); + $response->assertJsonPath('success', true); + $response->assertJsonPath('data.employer.name', 'Employer One'); + $response->assertJsonPath('data.employer.email', null); // Masked/null since no connection exists + $response->assertJsonPath('data.employer.phone', null); + } } diff --git a/tests/Feature/WorkerReviewTest.php b/tests/Feature/WorkerReviewTest.php index b38d3c8..2574366 100644 --- a/tests/Feature/WorkerReviewTest.php +++ b/tests/Feature/WorkerReviewTest.php @@ -268,4 +268,37 @@ public function test_view_and_list_reviews() $response->assertJsonCount(1, 'data.reviews'); $response->assertJsonPath('data.reviews.0.title', 'Awesome place'); } + + public function test_review_with_invalid_or_missing_job_id_passes_and_saves_null() + { + // Setup job application with hired status and joining_confirmed_at 3.5 months ago + JobApplication::create([ + 'job_id' => $this->job->id, + 'worker_id' => $this->worker->id, + 'status' => 'hired', + 'joining_confirmed_at' => now()->subMonths(3)->subDays(15)->toDateString(), + ]); + + // Act: Post review with an invalid job_id (e.g. 'invalid-id-or-empty') + $response = $this->postJson('/api/workers/reviews', [ + 'employer_id' => $this->employer->id, + 'job_id' => 'invalid-id-or-empty', + 'rating' => 4, + 'title' => 'Fair Employer', + 'comment' => 'Very good experience overall.', + ], [ + 'Authorization' => 'Bearer worker_api_token' + ]); + + $response->assertStatus(201); + $response->assertJsonPath('success', true); + $response->assertJsonPath('data.review.job_id', null); // Resolved to null + + $this->assertDatabaseHas('employer_reviews', [ + 'worker_id' => $this->worker->id, + 'employer_id' => $this->employer->id, + 'job_id' => null, + 'rating' => 4, + ]); + } }