diff --git a/app/Http/Controllers/Admin/WorkerController.php b/app/Http/Controllers/Admin/WorkerController.php index e5f8b79..1ac3ee6 100644 --- a/app/Http/Controllers/Admin/WorkerController.php +++ b/app/Http/Controllers/Admin/WorkerController.php @@ -42,7 +42,6 @@ public function index() 'country' => $worker->country, 'city' => $worker->city, 'area' => $worker->area, - 'preferred_location' => $worker->preferred_location, 'live_in_out' => $worker->live_in_out ?? 'Live-in', 'category' => $worker->category ? $worker->category->name : 'N/A', 'experience' => $worker->experience, @@ -126,7 +125,6 @@ public function updateProfile(Request $request, $id) 'country' => 'nullable|string', 'city' => 'nullable|string', 'area' => 'nullable|string', - 'preferred_location' => 'nullable|string', 'live_in_out' => 'nullable|string', 'category' => 'required|string', 'experience' => 'required|string', @@ -148,7 +146,6 @@ public function updateProfile(Request $request, $id) $worker->country = $validated['country'] ?? $worker->country; $worker->city = $validated['city'] ?? $worker->city; $worker->area = $validated['area'] ?? $worker->area; - $worker->preferred_location = $validated['preferred_location'] ?? $worker->preferred_location; $worker->live_in_out = $validated['live_in_out'] ?? $worker->live_in_out; $worker->experience = $validated['experience']; $worker->salary = $validated['salary'] ?? $worker->salary; diff --git a/app/Http/Controllers/Api/EmployerAuthController.php b/app/Http/Controllers/Api/EmployerAuthController.php index 15edfdc..7ef63a7 100644 --- a/app/Http/Controllers/Api/EmployerAuthController.php +++ b/app/Http/Controllers/Api/EmployerAuthController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Models\User; use App\Models\EmployerProfile; +use App\Models\Sponsor; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; @@ -21,66 +22,129 @@ class EmployerAuthController extends Controller public function login(Request $request) { $validator = Validator::make($request->all(), [ - 'email' => 'required|email', + 'email' => 'nullable|string', + 'mobile' => 'nullable|string', 'password' => 'required|string', ]); + $validator->after(function ($validator) use ($request) { + if (!$request->filled('email') && !$request->filled('mobile')) { + $validator->errors()->add('mobile', 'Either mobile number or email is required.'); + } + }); + if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', - 'errors' => $validator->errors() + 'errors' => $validator->errors() ], 422); } try { - $user = User::where('email', $request->email)->where('role', 'employer')->first(); + $loginValue = $request->input('mobile') ?? $request->input('email'); - if (!$user || !Hash::check($request->password, $user->password)) { + $sponsor = Sponsor::where('mobile', $loginValue) + ->orWhere('email', $loginValue) + ->first(); + + if ($sponsor) { + $user = User::where('email', $sponsor->email) + ->where('role', 'employer') + ->first(); + } else { + $user = User::where('email', $loginValue) + ->where('role', 'employer') + ->first(); + } + + if (!$sponsor && !$user) { return response()->json([ 'success' => false, - 'message' => 'Invalid email or password.' + 'message' => 'Invalid credentials.' ], 401); } - $profile = EmployerProfile::where('user_id', $user->id)->first(); - $verification_status = $profile ? $profile->verification_status : 'approved'; + $passwordHash = $sponsor ? $sponsor->password : $user->password; - if ($verification_status === 'pending') { + if (!Hash::check($request->password, $passwordHash)) { return response()->json([ 'success' => false, - 'message' => 'Your account is pending verification.' + 'message' => 'Invalid credentials.' + ], 401); + } + + if ($sponsor && $sponsor->status === 'suspended') { + return response()->json([ + 'success' => false, + 'message' => 'Your account has been suspended. Please contact support.' ], 403); } - if ($verification_status === 'rejected') { - return response()->json([ - 'success' => false, - 'message' => 'Your account verification has been rejected.', - 'reason' => $profile->rejection_reason ?? 'Verification rejected.' - ], 403); - } - - // Generate and assign a fresh API token $apiToken = Str::random(80); - $user->update(['api_token' => $apiToken]); - return response()->json([ - 'success' => true, - 'message' => 'Employer logged in successfully.', - 'data' => [ - 'employer' => $user->load('employerProfile'), - 'token' => $apiToken - ] - ], 200); + if ($user) { + // Employer account + $profile = EmployerProfile::where('user_id', $user->id)->first(); + $verification_status = $profile ? $profile->verification_status : 'approved'; + + if ($verification_status === 'pending') { + return response()->json([ + 'success' => false, + 'message' => 'Your account is pending verification.' + ], 403); + } + + if ($verification_status === 'rejected') { + return response()->json([ + 'success' => false, + 'message' => 'Your account verification has been rejected.', + 'reason' => $profile->rejection_reason ?? 'Verification rejected.' + ], 403); + } + + $user->update(['api_token' => $apiToken]); + if ($sponsor) { + $sponsor->update([ + 'api_token' => $apiToken, + 'last_login_at' => now(), + ]); + } + + return response()->json([ + 'success' => true, + 'message' => 'Login successful.', + 'role' => 'employer', + 'data' => [ + 'employer' => $user->load('employerProfile'), + 'token' => $apiToken + ] + ], 200); + } else { + // Pure Sponsor account + $sponsor->update([ + 'api_token' => $apiToken, + 'last_login_at' => now(), + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Login successful.', + 'role' => 'sponsor', + 'data' => [ + 'sponsor' => $sponsor->makeHidden(['password', 'api_token']), + 'token' => $apiToken + ] + ], 200); + } } catch (\Exception $e) { - logger()->error('Mobile Employer Login Failure: ' . $e->getMessage()); + logger()->error('Mobile Login Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred during login. Please try again.', - 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } diff --git a/app/Http/Controllers/Api/SponsorAuthController.php b/app/Http/Controllers/Api/SponsorAuthController.php new file mode 100644 index 0000000..42fc782 --- /dev/null +++ b/app/Http/Controllers/Api/SponsorAuthController.php @@ -0,0 +1,162 @@ +all(), [ + 'full_name' => 'required|string|max:255', + 'mobile' => 'required|string|max:50|unique:sponsors,mobile', + 'password' => 'required|string|min:6', + 'license_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240', + 'organization_name' => 'nullable|string|max:255', + 'email' => 'nullable|email|max:255|unique:sponsors,email', + 'nationality' => 'nullable|string|max:100', + 'city' => 'nullable|string|max:100', + 'address' => 'nullable|string|max:255', + 'country_code' => 'nullable|string|max:10', + ], [ + 'mobile.unique' => 'This mobile number is already registered.', + 'email.unique' => 'This email address is already registered.', + 'license_file.required' => 'Please upload your organization or trade license.', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Auto-generate email if not provided + $mobileClean = preg_replace('/[^0-9]/', '', $request->mobile); + $email = $request->email ?? "sponsor.{$mobileClean}@migrant.ae"; + if (!$request->email && Sponsor::where('email', $email)->exists()) { + $email = "sponsor.{$mobileClean}." . rand(10, 99) . "@migrant.ae"; + } + + // Store license file + $destinationPath = public_path('uploads/licenses'); + if (!file_exists($destinationPath)) { + mkdir($destinationPath, 0755, true); + } + + $licenseFile = $request->file('license_file'); + $licenseFileName = time() . '_license_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $licenseFile->getClientOriginalName()); + $licenseFile->move($destinationPath, $licenseFileName); + $licensePath = 'uploads/licenses/' . $licenseFileName; + + $apiToken = Str::random(80); + + $sponsor = DB::transaction(function () use ($request, $email, $licensePath, $apiToken) { + return Sponsor::create([ + 'full_name' => $request->full_name, + 'organization_name' => $request->organization_name, + 'email' => $email, + 'mobile' => $request->mobile, + 'password' => Hash::make($request->password), + 'country_code' => $request->country_code, + 'nationality' => $request->nationality, + 'city' => $request->city, + 'address' => $request->address, + 'license_file' => $licensePath, + 'status' => 'active', + 'is_verified' => false, + 'subscription_status' => 'none', + 'api_token' => $apiToken, + ]); + }); + + return response()->json([ + 'success' => true, + 'message' => 'Sponsor account registered successfully. Your license is pending admin review.', + 'data' => [ + 'sponsor' => $sponsor->makeHidden(['password', 'api_token']), + 'token' => $apiToken, + ] + ], 201); + + } catch (\Exception $e) { + logger()->error('Sponsor Registration Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred during registration. Please try again.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * Authenticate a Sponsor and return their bearer token. + * + * POST /api/sponsors/login + */ + public function login(Request $request) + { + $validator = Validator::make($request->all(), [ + 'mobile' => 'required|string', + 'password' => 'required|string', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + $sponsor = Sponsor::where('mobile', $request->mobile)->first(); + + if (!$sponsor || !Hash::check($request->password, $sponsor->password)) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid mobile number or password.' + ], 401); + } + + if ($sponsor->status === 'suspended') { + return response()->json([ + 'success' => false, + 'message' => 'Your account has been suspended. Please contact support.' + ], 403); + } + + // Rotate token on each login for security + $apiToken = Str::random(80); + $sponsor->update([ + 'api_token' => $apiToken, + 'last_login_at' => now(), + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Login successful.', + 'data' => [ + 'sponsor' => $sponsor->makeHidden(['password', 'api_token']), + 'token' => $apiToken, + ] + ], 200); + } +} diff --git a/app/Http/Controllers/Api/SponsorController.php b/app/Http/Controllers/Api/SponsorController.php new file mode 100644 index 0000000..a1ae18b --- /dev/null +++ b/app/Http/Controllers/Api/SponsorController.php @@ -0,0 +1,160 @@ +attributes->get('sponsor'); + + try { + // Recent charity/update announcements for the dashboard preview + $recentEvents = Announcement::latest() + ->limit(5) + ->get() + ->map(function ($event) { + return [ + 'id' => $event->id, + 'title' => $event->title, + 'body' => $event->body, + 'type' => $event->type, + 'created_at' => $event->created_at->toIso8601String(), + 'time_ago' => $event->created_at->diffForHumans(), + ]; + }); + + return response()->json([ + 'success' => true, + 'data' => [ + 'sponsor' => [ + 'id' => $sponsor->id, + 'full_name' => $sponsor->full_name, + 'organization_name' => $sponsor->organization_name, + 'mobile' => $sponsor->mobile, + 'email' => $sponsor->email, + 'city' => $sponsor->city, + 'nationality' => $sponsor->nationality, + 'is_verified' => $sponsor->is_verified, + 'status' => $sponsor->status, + 'license_file' => $sponsor->license_file ? asset($sponsor->license_file) : null, + 'joined_at' => $sponsor->created_at->toIso8601String(), + ], + 'recent_charity_events' => $recentEvents, + 'total_events' => Announcement::count(), + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Sponsor Dashboard API Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while loading your dashboard.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * GET /api/sponsors/charity-events + * + * Returns a paginated list of all charity/announcement events. + */ + public function getCharityEvents(Request $request) + { + try { + $page = (int) $request->input('page', 1); + $perPage = (int) $request->input('per_page', 15); + $type = $request->input('type'); // optional filter: 'charity', 'update', etc. + + $query = Announcement::with('employer.employerProfile')->latest(); + + if ($type) { + $query->where('type', $type); + } + + $total = $query->count(); + $offset = ($page - 1) * $perPage; + + $events = $query->skip($offset)->take($perPage)->get() + ->map(function ($event) { + return [ + 'id' => $event->id, + 'title' => $event->title, + 'body' => $event->body, + 'type' => $event->type, + 'posted_by' => $event->employer->name ?? 'System', + 'organization' => optional($event->employer)->employerProfile->company_name ?? 'Migrant Support', + 'created_at' => $event->created_at->toIso8601String(), + 'time_ago' => $event->created_at->diffForHumans(), + ]; + }); + + return response()->json([ + 'success' => true, + 'data' => [ + 'events' => $events, + 'pagination' => [ + 'total' => $total, + 'per_page' => $perPage, + 'current_page' => $page, + 'last_page' => max(1, (int) ceil($total / $perPage)), + ], + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Sponsor Charity Events API Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while fetching charity events.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * GET /api/sponsors/profile + * + * Returns the authenticated sponsor's full profile. + */ + public function getProfile(Request $request) + { + /** @var Sponsor $sponsor */ + $sponsor = $request->attributes->get('sponsor'); + + return response()->json([ + 'success' => true, + 'data' => [ + 'sponsor' => [ + 'id' => $sponsor->id, + 'full_name' => $sponsor->full_name, + 'organization_name' => $sponsor->organization_name, + 'mobile' => $sponsor->mobile, + 'email' => $sponsor->email, + 'nationality' => $sponsor->nationality, + 'city' => $sponsor->city, + 'address' => $sponsor->address, + 'country_code' => $sponsor->country_code, + 'is_verified' => $sponsor->is_verified, + 'status' => $sponsor->status, + 'license_file' => $sponsor->license_file ? asset($sponsor->license_file) : null, + 'joined_at' => $sponsor->created_at->toIso8601String(), + ] + ] + ], 200); + } +} diff --git a/app/Http/Controllers/Api/WorkerAuthController.php b/app/Http/Controllers/Api/WorkerAuthController.php index 40a76d1..3f0c185 100644 --- a/app/Http/Controllers/Api/WorkerAuthController.php +++ b/app/Http/Controllers/Api/WorkerAuthController.php @@ -292,6 +292,13 @@ public function register(Request $request) 'nationality' => 'nullable|string|max:100', 'age' => 'nullable|integer', 'experience' => 'nullable|string|max:100', + 'in_country' => 'nullable', + 'visa_status' => 'nullable|string|max:100', + 'preferred_job_type' => 'nullable|string|max:100', + 'live_in_out' => 'nullable|string|max:100', + 'country' => 'nullable|string|max:100', + 'city' => 'nullable|string|max:100', + 'area' => 'nullable|string|max:100', ]); if ($validator->fails()) { @@ -344,9 +351,10 @@ public function register(Request $request) $visaPath = 'uploads/documents/' . $fileName; } + $inCountry = filter_var($request->input('in_country', true), FILTER_VALIDATE_BOOLEAN); $apiToken = Str::random(80); - $result = DB::transaction(function () use ($request, $email, $skillsArray, $passportPath, $visaPath, $apiToken) { + $result = DB::transaction(function () use ($request, $email, $skillsArray, $passportPath, $visaPath, $apiToken, $inCountry) { $worker = Worker::create([ 'name' => $request->name, 'email' => $email, @@ -364,6 +372,13 @@ public function register(Request $request) 'verified' => false, 'status' => 'active', 'api_token' => $apiToken, + 'in_country' => $inCountry, + 'visa_status' => $inCountry ? $request->visa_status : null, + 'preferred_job_type' => $request->preferred_job_type, + 'live_in_out' => $request->live_in_out, + 'country' => $request->country, + 'city' => $request->city, + 'area' => $request->area, ]); if (!empty($skillsArray)) { diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index fd3f3dc..fe55e87 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -61,6 +61,13 @@ public function updateProfile(Request $request) 'category_id' => 'nullable|exists:worker_categories,id', 'skills' => 'nullable|array', 'skills.*' => 'exists:skills,id', + 'in_country' => 'nullable', + 'visa_status' => 'nullable|string|max:100', + 'preferred_job_type' => 'nullable|string|max:100', + 'live_in_out' => 'nullable|string|max:100', + 'country' => 'nullable|string|max:100', + 'city' => 'nullable|string|max:100', + 'area' => 'nullable|string|max:100', ]); if ($validator->fails()) { @@ -84,7 +91,13 @@ public function updateProfile(Request $request) 'experience', 'religion', 'bio', - 'category_id' + 'category_id', + 'visa_status', + 'preferred_job_type', + 'live_in_out', + 'country', + 'city', + 'area' ]); // Filter out null inputs if we want to support partial updates @@ -92,6 +105,10 @@ public function updateProfile(Request $request) return !is_null($value); }); + if ($request->has('in_country')) { + $updateData['in_country'] = filter_var($request->input('in_country'), FILTER_VALIDATE_BOOLEAN); + } + $worker->update($updateData); // Sync skills if provided diff --git a/app/Http/Middleware/SponsorApiMiddleware.php b/app/Http/Middleware/SponsorApiMiddleware.php new file mode 100644 index 0000000..80c4dfc --- /dev/null +++ b/app/Http/Middleware/SponsorApiMiddleware.php @@ -0,0 +1,47 @@ +header('Authorization'); + + if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) { + return response()->json([ + 'success' => false, + 'message' => 'Authentication token required.' + ], 401); + } + + $token = substr($authHeader, 7); + $sponsor = Sponsor::where('api_token', $token)->first(); + + if (!$sponsor) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid or expired authentication token.' + ], 401); + } + + if ($sponsor->status === 'suspended') { + return response()->json([ + 'success' => false, + 'message' => 'Your account has been suspended. Please contact support.' + ], 403); + } + + // Attach sponsor to request for use in controllers + $request->attributes->set('sponsor', $sponsor); + + return $next($request); + } +} diff --git a/app/Models/Sponsor.php b/app/Models/Sponsor.php index 163fa42..418d827 100644 --- a/app/Models/Sponsor.php +++ b/app/Models/Sponsor.php @@ -12,6 +12,7 @@ class Sponsor extends Authenticatable protected $fillable = [ 'full_name', + 'organization_name', 'email', 'mobile', 'password', @@ -29,10 +30,13 @@ class Sponsor extends Authenticatable 'nationality', 'status', 'last_login_at', + 'api_token', + 'license_file', ]; protected $hidden = [ 'password', + 'api_token', ]; protected $casts = [ diff --git a/app/Models/Worker.php b/app/Models/Worker.php index 12f51a2..d8c46d2 100644 --- a/app/Models/Worker.php +++ b/app/Models/Worker.php @@ -20,7 +20,6 @@ class Worker extends Model 'country', 'city', 'area', - 'preferred_location', 'live_in_out', 'age', 'gender', @@ -33,6 +32,9 @@ class Worker extends Model 'verified', 'status', 'api_token', + 'in_country', + 'visa_status', + 'preferred_job_type', ]; protected $hidden = [ @@ -42,6 +44,7 @@ class Worker extends Model protected $casts = [ 'verified' => 'boolean', + 'in_country' => 'boolean', 'salary' => 'decimal:2', 'password' => 'hashed', ]; @@ -63,6 +66,9 @@ public function getPassportStatusAttribute() public function getVisaStatusAttribute() { + if ($this->attributes['visa_status'] ?? null) { + return $this->attributes['visa_status']; + } $visa = $this->documents->where('type', 'visa')->first(); if (!$visa) { return 'Visa Pending'; diff --git a/bootstrap/app.php b/bootstrap/app.php index 278de26..452c260 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -17,10 +17,11 @@ \App\Http\Middleware\HandleInertiaRequests::class, ]); $middleware->alias([ - 'admin' => \App\Http\Middleware\AdminMiddleware::class, - 'employer' => \App\Http\Middleware\EmployerMiddleware::class, - 'auth.worker' => \App\Http\Middleware\WorkerApiMiddleware::class, - 'auth.employer' => \App\Http\Middleware\EmployerApiMiddleware::class, + 'admin' => \App\Http\Middleware\AdminMiddleware::class, + 'employer' => \App\Http\Middleware\EmployerMiddleware::class, + 'auth.worker' => \App\Http\Middleware\WorkerApiMiddleware::class, + 'auth.employer'=> \App\Http\Middleware\EmployerApiMiddleware::class, + 'auth.sponsor' => \App\Http\Middleware\SponsorApiMiddleware::class, ]); }) ->withExceptions(function (Exceptions $exceptions): void { diff --git a/database/migrations/2026_06_05_150000_add_api_fields_to_workers_table.php b/database/migrations/2026_06_05_150000_add_api_fields_to_workers_table.php new file mode 100644 index 0000000..8ad56ca --- /dev/null +++ b/database/migrations/2026_06_05_150000_add_api_fields_to_workers_table.php @@ -0,0 +1,36 @@ +boolean('in_country')->default(true)->after('nationality'); + } + if (!Schema::hasColumn('workers', 'visa_status')) { + $table->string('visa_status')->nullable()->after('in_country'); + } + if (!Schema::hasColumn('workers', 'preferred_job_type')) { + $table->string('preferred_job_type')->nullable()->after('live_in_out'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('workers', function (Blueprint $table) { + $table->dropColumn(['in_country', 'visa_status', 'preferred_job_type']); + }); + } +}; diff --git a/database/migrations/2026_06_05_160000_add_api_fields_to_sponsors_table.php b/database/migrations/2026_06_05_160000_add_api_fields_to_sponsors_table.php new file mode 100644 index 0000000..5dd00d1 --- /dev/null +++ b/database/migrations/2026_06_05_160000_add_api_fields_to_sponsors_table.php @@ -0,0 +1,30 @@ +string('api_token', 100)->nullable()->unique()->after('status'); + } + if (!Schema::hasColumn('sponsors', 'license_file')) { + $table->string('license_file')->nullable()->after('api_token'); + } + if (!Schema::hasColumn('sponsors', 'organization_name')) { + $table->string('organization_name')->nullable()->after('full_name'); + } + }); + } + + public function down(): void + { + Schema::table('sponsors', function (Blueprint $table) { + $table->dropColumn(['api_token', 'license_file', 'organization_name']); + }); + } +}; diff --git a/public/swagger.json b/public/swagger.json index 946ac40..2858454 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -17,6 +17,216 @@ } ], "paths": { + "/sponsors/register": { + "post": { + "tags": ["Sponsor/Auth"], + "summary": "Register Sponsor Account", + "description": "Registers a new sponsor with basic information and uploads their organization/trade license. Sponsors have restricted access — dashboard and charity events only. No payment required.", + "security": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": ["full_name", "mobile", "password", "license_file"], + "properties": { + "full_name": { + "type": "string", + "example": "Mohammed Al-Rashidi", + "description": "Full legal name of the sponsor. (REQUIRED)" + }, + "mobile": { + "type": "string", + "example": "+971501112233", + "description": "Mobile phone number — must be unique. (REQUIRED)" + }, + "password": { + "type": "string", + "format": "password", + "example": "securepass123", + "description": "Login password (min 6 characters). (REQUIRED)" + }, + "license_file": { + "type": "string", + "format": "binary", + "description": "Organization or trade license document (jpg/png/pdf, max 10MB). (REQUIRED)" + }, + "organization_name": { + "type": "string", + "example": "Al-Rashidi Charitable Foundation", + "description": "Name of the sponsoring organization." + }, + "email": { + "type": "string", + "format": "email", + "example": "info@alrashidi.ae", + "description": "Email address (optional — auto-generated if not provided)." + }, + "nationality": { + "type": "string", + "example": "UAE", + "description": "Nationality of the sponsor." + }, + "city": { + "type": "string", + "example": "Dubai", + "description": "City of residence." + }, + "address": { + "type": "string", + "example": "Villa 12, Jumeirah 2", + "description": "Physical address." + }, + "country_code": { + "type": "string", + "example": "+971", + "description": "Country dial code." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Sponsor registered successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": {"type": "boolean", "example": true}, + "message": {"type": "string", "example": "Sponsor account registered successfully. Your license is pending admin review."}, + "data": { + "type": "object", + "properties": { + "sponsor": {"$ref": "#/components/schemas/Sponsor"}, + "token": {"type": "string", "example": "abc123...bearerToken..."} + } + } + } + } + } + } + }, + "422": {"description": "Validation error (duplicate mobile/email, missing required fields)."} + } + } + }, + "/sponsors/login": { + "post": { + "tags": [ + "Sponsor/Auth" + ], + "summary": "Authenticate Sponsor or Employer", + "description": "Unified authentication endpoint. Validates email/mobile and password, then returns a stateless Bearer token and the user's role (employer or sponsor). Same behavior as /employers/login.", + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "password" + ], + "properties": { + "email": { + "type": "string", + "example": "ahmad@example.com", + "description": "Email address (required if mobile is not provided)" + }, + "mobile": { + "type": "string", + "example": "+971509990001", + "description": "Mobile number (required if email is not provided)" + }, + "password": { + "type": "string", + "example": "Password@123" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Login successful. Returns Bearer token and role.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Login successful." + }, + "role": { + "type": "string", + "example": "sponsor", + "enum": ["employer", "sponsor"] + }, + "data": { + "type": "object", + "properties": { + "token": { + "type": "string", + "example": "api_token_here" + } + } + } + } + } + } + } + } + } + } + }, + "/sponsors/dashboard": { + "get": { + "tags": ["Sponsor"], + "summary": "Sponsor Dashboard", + "description": "Returns the sponsor profile summary and a preview of the latest charity events.", + "responses": { + "200": {"description": "Dashboard data retrieved successfully."}, + "401": {"description": "Unauthenticated."} + } + } + }, + "/sponsors/charity-events": { + "get": { + "tags": ["Sponsor"], + "summary": "List Charity Events", + "description": "Returns a paginated list of all charity and announcement events. Optionally filter by type.", + "parameters": [ + {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, + {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 15}}, + {"name": "type", "in": "query", "schema": {"type": "string"}, "description": "Optional event type filter (e.g. 'charity', 'update')."} + ], + "responses": { + "200": {"description": "Events retrieved successfully."}, + "401": {"description": "Unauthenticated."} + } + } + }, + "/sponsors/profile": { + "get": { + "tags": ["Sponsor"], + "summary": "Get Sponsor Profile", + "description": "Returns the full profile of the authenticated sponsor.", + "responses": { + "200": {"description": "Profile retrieved successfully."}, + "401": {"description": "Unauthenticated."} + } + } + }, "/workers/register": { "post": { "tags": [ @@ -101,6 +311,41 @@ "type": "string", "example": "4 Years", "description": "Years of experience." + }, + "in_country": { + "type": "boolean", + "example": true, + "description": "Whether the worker is currently in-country (true) or out-country (false)." + }, + "visa_status": { + "type": "string", + "example": "Tourist Visa", + "description": "Visa status (only required/applicable if in_country is true)." + }, + "preferred_job_type": { + "type": "string", + "example": "full-time", + "description": "Preferred job type." + }, + "live_in_out": { + "type": "string", + "example": "Live-out", + "description": "Accommodation preference (e.g. 'Live-in', 'Live-out')." + }, + "country": { + "type": "string", + "example": "UAE", + "description": "Preferred location country." + }, + "city": { + "type": "string", + "example": "Dubai", + "description": "Preferred location city." + }, + "area": { + "type": "string", + "example": "Marina", + "description": "Preferred location area." } } } @@ -1277,53 +1522,6 @@ } } }, - "/sponsors/register": { - "post": { - "tags": [ - "Sponsor/Auth" - ], - "summary": "Register Sponsor", - "description": "Registers a new sponsor profile with basic info (Name, Email, Phone).", - "security": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "name", - "email", - "phone" - ], - "properties": { - "name": { - "type": "string", - "example": "Ahmad Bin Ahmed", - "description": "Sponsor's full name" - }, - "email": { - "type": "string", - "example": "ahmad@example.com", - "description": "Sponsor's contact email address" - }, - "phone": { - "type": "string", - "example": "+971509990001", - "description": "Sponsor's mobile phone number" - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Sponsor registered successfully." - } - } - } - }, "/employers/verify": { "post": { "tags": [ @@ -1364,46 +1562,6 @@ } } }, - "/sponsors/verify": { - "post": { - "tags": [ - "Sponsor/Auth" - ], - "summary": "Verify Sponsor Email OTP (Sponsor Prefix)", - "description": "Verifies the email address with the OTP verification code (Step 2).", - "security": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "email", - "otp" - ], - "properties": { - "email": { - "type": "string", - "example": "ahmad@example.com" - }, - "otp": { - "type": "string", - "example": "111111", - "description": "6-digit OTP code" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Email verified successfully." - } - } - } - }, "/employers/payment": { "post": { "tags": [ @@ -1453,55 +1611,6 @@ } } }, - "/sponsors/payment": { - "post": { - "tags": [ - "Sponsor/Auth" - ], - "summary": "Sponsor Subscription Payment (Sponsor Prefix)", - "description": "Submits a successful plan selection and PayTabs payment confirmation (Step 3).", - "security": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "email", - "plan_id", - "amount_aed", - "paytabs_transaction_id" - ], - "properties": { - "email": { - "type": "string", - "example": "ahmad@example.com" - }, - "plan_id": { - "type": "string", - "example": "premium" - }, - "amount_aed": { - "type": "number", - "example": 199 - }, - "paytabs_transaction_id": { - "type": "string", - "example": "TXN998877" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Subscription payment confirmed." - } - } - } - }, "/employers/password": { "post": { "tags": [ @@ -1548,52 +1657,6 @@ } } }, - "/sponsors/password": { - "post": { - "tags": [ - "Sponsor/Auth" - ], - "summary": "Create Sponsor Account Password (Sponsor Prefix)", - "description": "Configures and finalizes the portal login password to complete registration (Step 4). Returns bearer token.", - "security": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "email", - "password", - "password_confirmation" - ], - "properties": { - "email": { - "type": "string", - "example": "ahmad@example.com" - }, - "password": { - "type": "string", - "format": "password", - "example": "Password@123" - }, - "password_confirmation": { - "type": "string", - "format": "password", - "example": "Password@123" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Password set and registration finalized successfully." - } - } - } - }, "/employers/plans": { "get": { "tags": [ @@ -1609,28 +1672,13 @@ } } }, - "/sponsors/plans": { - "get": { - "tags": [ - "Sponsor/Auth" - ], - "summary": "Get Available Subscription Plans (Sponsor Prefix)", - "description": "Returns details (price, features) of subscription packages available to sponsors.", - "security": [], - "responses": { - "200": { - "description": "Subscription plans list retrieved successfully." - } - } - } - }, "/employers/login": { "post": { "tags": [ "Employer/Auth" ], - "summary": "Authenticate Employer", - "description": "Validates employer email and password and returns a stateless Bearer token.", + "summary": "Authenticate Employer or Sponsor", + "description": "Unified authentication endpoint. Validates email/mobile and password, then returns a stateless Bearer token and the user's role (employer or sponsor).", "security": [], "requestBody": { "required": true, @@ -1639,13 +1687,18 @@ "schema": { "type": "object", "required": [ - "email", "password" ], "properties": { "email": { "type": "string", - "example": "ahmad@example.com" + "example": "ahmad@example.com", + "description": "Email address (required if mobile is not provided)" + }, + "mobile": { + "type": "string", + "example": "+971509990001", + "description": "Mobile number (required if email is not provided)" }, "password": { "type": "string", @@ -1658,7 +1711,38 @@ }, "responses": { "200": { - "description": "Employer logged in successfully." + "description": "Login successful. Returns Bearer token and role.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Login successful." + }, + "role": { + "type": "string", + "example": "employer", + "enum": ["employer", "sponsor"] + }, + "data": { + "type": "object", + "properties": { + "token": { + "type": "string", + "example": "api_token_here" + } + } + } + } + } + } + } } } } @@ -3086,6 +3170,24 @@ } }, "schemas": { + "Sponsor": { + "type": "object", + "properties": { + "id": {"type": "integer", "example": 1}, + "full_name": {"type": "string", "example": "Mohammed Al-Rashidi"}, + "organization_name": {"type": "string", "example": "Al-Rashidi Charitable Foundation"}, + "email": {"type": "string", "example": "sponsor.971501112233@migrant.ae"}, + "mobile": {"type": "string", "example": "+971501112233"}, + "nationality": {"type": "string", "example": "UAE"}, + "city": {"type": "string", "example": "Dubai"}, + "address": {"type": "string", "example": "Villa 12, Jumeirah 2"}, + "country_code": {"type": "string", "example": "+971"}, + "is_verified": {"type": "boolean", "example": false}, + "status": {"type": "string", "example": "active"}, + "license_file": {"type": "string", "example": "uploads/licenses/1234567890_license_trade.pdf"}, + "created_at": {"type": "string", "format": "date-time"} + } + }, "Worker": { "type": "object", "properties": { @@ -3145,6 +3247,34 @@ "type": "string", "example": "active" }, + "in_country": { + "type": "boolean", + "example": true + }, + "visa_status": { + "type": "string", + "example": "Tourist Visa" + }, + "preferred_job_type": { + "type": "string", + "example": "full-time" + }, + "live_in_out": { + "type": "string", + "example": "Live-out" + }, + "country": { + "type": "string", + "example": "UAE" + }, + "city": { + "type": "string", + "example": "Dubai" + }, + "area": { + "type": "string", + "example": "Marina" + }, "created_at": { "type": "string", "format": "date-time", diff --git a/public/uploads/documents/1780653554_passport_1000100671.jpg b/public/uploads/documents/1780653554_passport_1000100671.jpg new file mode 100644 index 0000000..66666c9 Binary files /dev/null and b/public/uploads/documents/1780653554_passport_1000100671.jpg differ diff --git a/public/uploads/documents/1780653554_visa_1000100465.jpg b/public/uploads/documents/1780653554_visa_1000100465.jpg new file mode 100644 index 0000000..331702a Binary files /dev/null and b/public/uploads/documents/1780653554_visa_1000100465.jpg differ diff --git a/public/uploads/documents/1780654781_passport_passport.pdf b/public/uploads/documents/1780654781_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780654844_passport_passport.pdf b/public/uploads/documents/1780654844_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780655203_passport_passport.pdf b/public/uploads/documents/1780655203_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780655685_passport_passport.pdf b/public/uploads/documents/1780655685_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780655814_passport_mds.jpeg b/public/uploads/documents/1780655814_passport_mds.jpeg new file mode 100644 index 0000000..cb5f3e9 Binary files /dev/null and b/public/uploads/documents/1780655814_passport_mds.jpeg differ diff --git a/public/uploads/documents/1780655814_visa_mds12.png b/public/uploads/documents/1780655814_visa_mds12.png new file mode 100644 index 0000000..ed4195b Binary files /dev/null and b/public/uploads/documents/1780655814_visa_mds12.png differ diff --git a/public/uploads/documents/1780656536_passport_passport.pdf b/public/uploads/documents/1780656536_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780656832_passport_passport.pdf b/public/uploads/documents/1780656832_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780656844_passport_passport.pdf b/public/uploads/documents/1780656844_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780656859_passport_passport.pdf b/public/uploads/documents/1780656859_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780656874_passport_passport.pdf b/public/uploads/documents/1780656874_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780656902_passport_passport.pdf b/public/uploads/documents/1780656902_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780657377_passport_passport.pdf b/public/uploads/documents/1780657377_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780656858_license_license.pdf b/public/uploads/licenses/1780656858_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780656874_license_license.pdf b/public/uploads/licenses/1780656874_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780656902_license_license.pdf b/public/uploads/licenses/1780656902_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780657376_license_license.pdf b/public/uploads/licenses/1780657376_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/routes/api.php b/routes/api.php index 7ab446b..ccc199b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -10,6 +10,8 @@ use App\Http\Controllers\Api\EmployerAnnouncementController; use App\Http\Controllers\Api\EmployerProfileController; use App\Http\Controllers\Api\EmployerReviewController; +use App\Http\Controllers\Api\SponsorAuthController; +use App\Http\Controllers\Api\SponsorController; /* |-------------------------------------------------------------------------- @@ -33,20 +35,17 @@ // Unprotected Employer Mobile Auth Endpoints Route::post('/employers/register', [EmployerAuthController::class, 'register']); -Route::post('/sponsors/register', [EmployerAuthController::class, 'register']); Route::post('/employers/verify', [EmployerAuthController::class, 'verify']); -Route::post('/sponsors/verify', [EmployerAuthController::class, 'verify']); Route::post('/employers/payment', [EmployerAuthController::class, 'payment']); -Route::post('/sponsors/payment', [EmployerAuthController::class, 'payment']); Route::post('/employers/password', [EmployerAuthController::class, 'password']); -Route::post('/sponsors/password', [EmployerAuthController::class, 'password']); Route::get('/employers/plans', [EmployerAuthController::class, 'plans']); -Route::get('/sponsors/plans', [EmployerAuthController::class, 'plans']); Route::post('/employers/login', [EmployerAuthController::class, 'login']); Route::post('/employers/forgot-password', [EmployerAuthController::class, 'forgotPassword']); -Route::post('/sponsors/forgot-password', [EmployerAuthController::class, 'forgotPassword']); Route::post('/employers/reset-password', [EmployerAuthController::class, 'resetPassword']); -Route::post('/sponsors/reset-password', [EmployerAuthController::class, 'resetPassword']); + +// Sponsor Mobile Auth Endpoints (unprotected) +Route::post('/sponsors/register', [SponsorAuthController::class, 'register']); +Route::post('/sponsors/login', [EmployerAuthController::class, 'login']); // Protected Worker Mobile Endpoints (Token Authenticated via Bearer Token) @@ -119,3 +118,11 @@ Route::post('/employers/report', [\App\Http\Controllers\Api\ReportController::class, 'reportFromEmployer']); Route::get('/employers/report-reasons', [\App\Http\Controllers\Api\ReportController::class, 'getReasonsForEmployer']); }); + +// Protected Sponsor Mobile Endpoints (Token Authenticated via Bearer Token) +// Sponsors can ONLY access: dashboard and charity events. No payments, no worker browsing. +Route::middleware(['auth.sponsor'])->group(function () { + Route::get('/sponsors/profile', [SponsorController::class, 'getProfile']); + Route::get('/sponsors/dashboard', [SponsorController::class, 'getDashboard']); + Route::get('/sponsors/charity-events', [SponsorController::class, 'getCharityEvents']); +}); diff --git a/tests/Feature/SponsorAuthApiTest.php b/tests/Feature/SponsorAuthApiTest.php new file mode 100644 index 0000000..a2c9681 --- /dev/null +++ b/tests/Feature/SponsorAuthApiTest.php @@ -0,0 +1,168 @@ +create('license.pdf', 500, 'application/pdf'); + + $response = $this->postJson('/api/sponsors/register', [ + 'full_name' => 'Test Sponsor Organization', + 'mobile' => '+971509990001', + 'password' => 'securepassword', + 'license_file' => $licenseFile, + 'organization_name' => 'Charity Co', + 'nationality' => 'Emirati', + 'city' => 'Abu Dhabi', + ]); + + $response->assertStatus(201) + ->assertJsonStructure([ + 'success', + 'message', + 'data' => [ + 'sponsor' => [ + 'id', + 'full_name', + 'mobile', + 'organization_name', + 'nationality', + 'city', + 'license_file', + ], + 'token', + ] + ]); + + $this->assertDatabaseHas('sponsors', [ + 'mobile' => '+971509990001', + 'full_name' => 'Test Sponsor Organization', + ]); + } + + /** + * Test both Sponsor and Employer can log in using the unified login endpoint. + */ + public function test_unified_login_returns_correct_role_and_data() + { + // 1. Create a pure Sponsor (no User record) + $sponsorUser = Sponsor::create([ + 'full_name' => 'Sponsor User', + 'email' => 'sponsor@example.com', + 'mobile' => '+971509991111', + 'password' => Hash::make('password123'), + 'license_file' => 'uploads/licenses/fake.pdf', + 'status' => 'active', + ]); + + // 2. Create an Employer (has both User and Sponsor records, legacy setup) + $employerUser = User::create([ + 'name' => 'Employer User', + 'email' => 'employer@example.com', + 'password' => Hash::make('password123'), + 'role' => 'employer', + 'api_token' => 'old_token', + ]); + + EmployerProfile::create([ + 'user_id' => $employerUser->id, + 'company_name' => 'Employer Corp', + 'phone' => '+971509992222', + 'country' => 'United Arab Emirates', + 'verification_status' => 'approved', + ]); + + Sponsor::create([ + 'full_name' => 'Employer User', + 'email' => 'employer@example.com', + 'mobile' => '+971509992222', + 'password' => $employerUser->password, + 'status' => 'active', + ]); + + // --- Test Sponsor Login via /api/employers/login --- + $responseSponsor = $this->postJson('/api/employers/login', [ + 'mobile' => '+971509991111', + 'password' => 'password123', + ]); + + $responseSponsor->assertStatus(200) + ->assertJson([ + 'success' => true, + 'role' => 'sponsor', + ]) + ->assertJsonStructure([ + 'data' => [ + 'sponsor', + 'token', + ] + ]); + + // --- Test Employer Login via /api/employers/login --- + $responseEmployer = $this->postJson('/api/employers/login', [ + 'mobile' => '+971509992222', + 'password' => 'password123', + ]); + + $responseEmployer->assertStatus(200) + ->assertJson([ + 'success' => true, + 'role' => 'employer', + ]) + ->assertJsonStructure([ + 'data' => [ + 'employer' => [ + 'id', + 'name', + 'email', + 'employer_profile', + ], + 'token', + ] + ]); + } + + /** + * Test that pure sponsors can only access their limited routes. + */ + public function test_sponsor_restricted_access() + { + $sponsor = Sponsor::create([ + 'full_name' => 'Pure Sponsor', + 'email' => 'pure_sponsor@example.com', + 'mobile' => '+971509993333', + 'password' => Hash::make('password123'), + 'api_token' => 'sponsor_token_abc', + 'status' => 'active', + ]); + + // Access dashboard successfully + $responseDash = $this->withHeaders([ + 'Authorization' => 'Bearer sponsor_token_abc', + ])->getJson('/api/sponsors/dashboard'); + + $responseDash->assertStatus(200); + + // Try to access employer protected route (should get blocked as they don't have employer token/role) + $responseEmployerRoute = $this->withHeaders([ + 'Authorization' => 'Bearer sponsor_token_abc', + ])->getJson('/api/employers/profile'); + + $responseEmployerRoute->assertStatus(401); + } +} diff --git a/tests/Feature/WorkerJourneyApiTest.php b/tests/Feature/WorkerJourneyApiTest.php index 018f562..98f9225 100644 --- a/tests/Feature/WorkerJourneyApiTest.php +++ b/tests/Feature/WorkerJourneyApiTest.php @@ -407,4 +407,95 @@ public function test_s6_outcome_mark_hired() 'status' => 'Hired', ]); } + + /** + * Test registration with new API fields. + */ + public function test_register_with_new_api_fields() + { + $fakePassport = UploadedFile::fake()->create('passport.pdf', 500, 'application/pdf'); + + $response = $this->postJson('/api/workers/register', [ + 'name' => 'John New Worker', + 'phone' => '+971501111112', + 'salary' => 2200.50, + 'password' => 'secret123', + 'passport_file' => $fakePassport, + 'language' => 'HI, SW', + 'nationality' => 'Kenya', + 'age' => 30, + 'experience' => '5 Years', + 'in_country' => 'true', + 'visa_status' => 'Tourist Visa', + 'preferred_job_type' => 'full-time', + 'live_in_out' => 'Live-out (External housing)', + 'country' => 'UAE', + 'city' => 'Dubai', + 'area' => 'Marina', + ]); + + $response->assertStatus(201); + + $this->assertDatabaseHas('workers', [ + 'name' => 'John New Worker', + 'phone' => '+971501111112', + 'in_country' => true, + 'visa_status' => 'Tourist Visa', + 'preferred_job_type' => 'full-time', + 'live_in_out' => 'Live-out (External housing)', + 'country' => 'UAE', + 'city' => 'Dubai', + 'area' => 'Marina', + ]); + } + + /** + * Test updating profile with new API fields. + */ + public function test_update_profile_with_new_api_fields() + { + $worker = Worker::create([ + 'name' => 'Original Worker', + 'email' => 'orig@example.com', + 'phone' => '+971502222223', + 'language' => 'SW', + 'password' => bcrypt('password'), + 'nationality' => 'Kenya', + 'age' => 22, + 'salary' => 1200, + 'availability' => 'Immediate', + 'experience' => 'None', + 'religion' => 'Christian', + 'bio' => 'New helper', + 'category_id' => $this->category->id, + 'verified' => false, + 'status' => 'active', + 'api_token' => 'bearer-update-token', + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer bearer-update-token', + ])->postJson('/api/workers/profile/update', [ + 'in_country' => false, + 'visa_status' => null, + 'preferred_job_type' => 'part-time', + 'live_in_out' => 'Live-in (Stay with family)', + 'country' => 'UAE', + 'city' => 'Abu Dhabi', + 'area' => 'Khalifa City', + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('workers', [ + 'id' => $worker->id, + 'in_country' => false, + 'visa_status' => null, + 'preferred_job_type' => 'part-time', + 'live_in_out' => 'Live-in (Stay with family)', + 'country' => 'UAE', + 'city' => 'Abu Dhabi', + 'area' => 'Khalifa City', + ]); + } }