From d2c3564c264bc0925100bf27fdbbc8c41b1144cb Mon Sep 17 00:00:00 2001 From: mohanmd Date: Sat, 20 Jun 2026 20:05:54 +0530 Subject: [PATCH] worker page profile update --- .../Api/WorkerProfileController.php | 173 +- public/swagger.json | 11037 ++++++++-------- .../licenses/1780656858_license_license.pdf | 0 .../licenses/1780656874_license_license.pdf | 0 .../licenses/1780656902_license_license.pdf | 0 .../licenses/1780657376_license_license.pdf | 0 .../licenses/1780658014_license_license.pdf | 0 .../licenses/1780910940_license_license.pdf | 0 .../licenses/1780911257_license_license.pdf | 0 .../licenses/1780911277_license_license.pdf | 0 .../licenses/1780911292_license_license.pdf | 0 .../licenses/1780911308_license_license.pdf | 0 .../licenses/1780911321_license_license.pdf | 0 .../licenses/1780912272_license_license.pdf | 0 .../licenses/1780912872_license_license.pdf | 0 .../licenses/1780914822_license_license.pdf | 0 .../licenses/1780988093_license_license.pdf | 0 .../licenses/1780988183_license_license.pdf | 0 .../licenses/1780988391_license_license.pdf | 0 .../licenses/1780988597_license_license.pdf | 0 .../licenses/1780988676_license_license.pdf | 0 .../licenses/1780988798_license_license.pdf | 0 .../licenses/1780988812_license_license.pdf | 0 .../licenses/1780988844_license_license.pdf | 0 .../licenses/1780989036_license_license.pdf | 0 .../licenses/1780989071_license_license.pdf | 0 .../licenses/1780989258_license_license.pdf | 0 .../licenses/1780990035_license_license.pdf | 0 .../licenses/1780990120_license_license.pdf | 0 .../licenses/1780990245_license_license.pdf | 0 .../licenses/1780990328_license_license.pdf | 0 .../licenses/1780990399_license_license.pdf | 0 .../licenses/1780990462_license_license.pdf | 0 .../licenses/1780990748_license_license.pdf | 0 .../licenses/1780991005_license_license.pdf | 0 .../licenses/1780991060_license_license.pdf | 0 ...80997733_license_fDi5IHoHP14P_1024_500.png | Bin 83228 -> 0 bytes .../licenses/1781082884_license_license.pdf | 0 .../licenses/1781082893_license_license.pdf | 0 .../licenses/1781087119_license_license.pdf | 0 .../licenses/1781087242_license_license.pdf | 0 .../licenses/1781087905_license_license.pdf | 0 .../licenses/1781087935_license_license.pdf | 0 .../licenses/1781087948_license_license.pdf | 0 .../licenses/1781088511_license_license.pdf | 0 .../licenses/1781088528_license_license.pdf | 0 .../licenses/1781088910_license_license.pdf | 0 .../licenses/1781088941_license_license.pdf | 0 .../licenses/1781092147_license_license.pdf | 0 .../licenses/1781092407_license_license.pdf | 0 resources/js/Pages/Admin/Workers/Index.jsx | 243 +- tests/Feature/WorkerJourneyApiTest.php | 211 +- vite.config.js | 2 +- 53 files changed, 6021 insertions(+), 5645 deletions(-) delete mode 100644 public/uploads/licenses/1780656858_license_license.pdf delete mode 100644 public/uploads/licenses/1780656874_license_license.pdf delete mode 100644 public/uploads/licenses/1780656902_license_license.pdf delete mode 100644 public/uploads/licenses/1780657376_license_license.pdf delete mode 100644 public/uploads/licenses/1780658014_license_license.pdf delete mode 100644 public/uploads/licenses/1780910940_license_license.pdf delete mode 100644 public/uploads/licenses/1780911257_license_license.pdf delete mode 100644 public/uploads/licenses/1780911277_license_license.pdf delete mode 100644 public/uploads/licenses/1780911292_license_license.pdf delete mode 100644 public/uploads/licenses/1780911308_license_license.pdf delete mode 100644 public/uploads/licenses/1780911321_license_license.pdf delete mode 100644 public/uploads/licenses/1780912272_license_license.pdf delete mode 100644 public/uploads/licenses/1780912872_license_license.pdf delete mode 100644 public/uploads/licenses/1780914822_license_license.pdf delete mode 100644 public/uploads/licenses/1780988093_license_license.pdf delete mode 100644 public/uploads/licenses/1780988183_license_license.pdf delete mode 100644 public/uploads/licenses/1780988391_license_license.pdf delete mode 100644 public/uploads/licenses/1780988597_license_license.pdf delete mode 100644 public/uploads/licenses/1780988676_license_license.pdf delete mode 100644 public/uploads/licenses/1780988798_license_license.pdf delete mode 100644 public/uploads/licenses/1780988812_license_license.pdf delete mode 100644 public/uploads/licenses/1780988844_license_license.pdf delete mode 100644 public/uploads/licenses/1780989036_license_license.pdf delete mode 100644 public/uploads/licenses/1780989071_license_license.pdf delete mode 100644 public/uploads/licenses/1780989258_license_license.pdf delete mode 100644 public/uploads/licenses/1780990035_license_license.pdf delete mode 100644 public/uploads/licenses/1780990120_license_license.pdf delete mode 100644 public/uploads/licenses/1780990245_license_license.pdf delete mode 100644 public/uploads/licenses/1780990328_license_license.pdf delete mode 100644 public/uploads/licenses/1780990399_license_license.pdf delete mode 100644 public/uploads/licenses/1780990462_license_license.pdf delete mode 100644 public/uploads/licenses/1780990748_license_license.pdf delete mode 100644 public/uploads/licenses/1780991005_license_license.pdf delete mode 100644 public/uploads/licenses/1780991060_license_license.pdf delete mode 100644 public/uploads/licenses/1780997733_license_fDi5IHoHP14P_1024_500.png delete mode 100644 public/uploads/licenses/1781082884_license_license.pdf delete mode 100644 public/uploads/licenses/1781082893_license_license.pdf delete mode 100644 public/uploads/licenses/1781087119_license_license.pdf delete mode 100644 public/uploads/licenses/1781087242_license_license.pdf delete mode 100644 public/uploads/licenses/1781087905_license_license.pdf delete mode 100644 public/uploads/licenses/1781087935_license_license.pdf delete mode 100644 public/uploads/licenses/1781087948_license_license.pdf delete mode 100644 public/uploads/licenses/1781088511_license_license.pdf delete mode 100644 public/uploads/licenses/1781088528_license_license.pdf delete mode 100644 public/uploads/licenses/1781088910_license_license.pdf delete mode 100644 public/uploads/licenses/1781088941_license_license.pdf delete mode 100644 public/uploads/licenses/1781092147_license_license.pdf delete mode 100644 public/uploads/licenses/1781092407_license_license.pdf diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index 916cb71..3af8bc4 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -30,6 +30,8 @@ public function getProfile(Request $request) $worker->load(['skills', 'documents']); + $worker->makeHidden(['email', 'religion', 'availability', 'bio', 'country', 'city', 'area']); + return response()->json([ 'success' => true, 'data' => [ @@ -49,16 +51,29 @@ public function updateProfile(Request $request) /** @var Worker $worker */ $worker = $request->attributes->get('worker'); + $data = $request->all(); + if (isset($data['passport']) && is_string($data['passport'])) { + $decoded = json_decode($data['passport'], true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { + $data['passport'] = $decoded; + } + } + if (isset($data['visa']) && is_string($data['visa'])) { + $decoded = json_decode($data['visa'], true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { + $data['visa'] = $decoded; + } + } + $request->merge($data); + $validator = Validator::make($request->all(), [ 'name' => 'nullable|string|max:255', + 'phone' => 'nullable|string|max:50|unique:workers,phone,' . $worker->id, 'age' => 'nullable|integer|min:18|max:70', 'nationality' => 'nullable|string|max:100', 'language' => 'nullable|string', 'salary' => 'nullable|numeric|min:0', - 'availability' => 'nullable|string|max:100', 'experience' => 'nullable|string|max:100', - 'religion' => 'nullable|string|max:100', - 'bio' => 'nullable|string|max:1000', 'skills' => 'nullable|array', 'skills.*' => 'exists:skills,id', 'in_country' => 'nullable', @@ -67,10 +82,19 @@ public function updateProfile(Request $request) 'gender' => 'nullable|string|in:male,female,other', 'live_in_out' => 'nullable|string|in:live_in,live_out', 'preferred_location' => 'nullable|string|max:255', - 'country' => 'nullable|string|max:100', - 'city' => 'nullable|string|max:100', - 'area' => 'nullable|string|max:100', 'fcm_token' => 'nullable|string|max:255', + 'passport' => 'nullable|array', + 'passport.passport_number' => [ + 'nullable', + 'string', + \Illuminate\Validation\Rule::unique('worker_documents', 'number')->where(function ($query) use ($worker) { + return $query->where('type', 'passport')->where('worker_id', '!=', $worker->id); + }), + ], + 'visa' => 'nullable|array', + ], [ + 'phone.unique' => 'This mobile number is already registered.', + 'passport.passport_number.unique' => 'This passport number is already registered.', ]); if ($validator->fails()) { @@ -81,27 +105,40 @@ public function updateProfile(Request $request) ], 422); } + // Document Update Check: Verify passport number matches existing passport number if already verified/saved + $existingPassport = $worker->documents()->where('type', 'passport')->first(); + if ($existingPassport && $request->has('passport')) { + $passportData = $request->input('passport'); + if ($passportData) { + $newPassportNumber = $passportData['passport_number'] ?? $passportData['number'] ?? null; + if ($newPassportNumber && strcasecmp(trim($existingPassport->number), trim($newPassportNumber)) !== 0) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => [ + 'passport.passport_number' => ['The passport number cannot be changed. It must match the previously verified passport number.'] + ] + ], 422); + } + } + } + try { - DB::transaction(function () use ($request, $worker) { + DB::transaction(function () use ($request, $worker, $existingPassport) { // Update worker attributes $updateData = $request->only([ 'name', + 'phone', 'age', 'nationality', 'language', 'salary', - 'availability', 'experience', - 'religion', - 'bio', 'visa_status', 'preferred_job_type', 'gender', 'live_in_out', 'preferred_location', - 'country', - 'city', - 'area', 'fcm_token' ]); @@ -120,10 +157,89 @@ public function updateProfile(Request $request) if ($request->has('skills')) { $worker->skills()->sync($request->skills ?: []); } + + // Process passport document creation/update + if ($request->has('passport')) { + $passportDataInput = $request->input('passport'); + if ($passportDataInput) { + $expiryDate = $passportDataInput['date_of_expiry'] ?? null; + $issueDate = $passportDataInput['date_of_issue'] ?? null; + $expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : null; + $issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : null; + + if ($existingPassport) { + $updateFields = [ + 'ocr_data' => $passportDataInput, + ]; + if ($expiryDateFormatted) { + $updateFields['expiry_date'] = $expiryDateFormatted; + } + if ($issueDateFormatted) { + $updateFields['issue_date'] = $issueDateFormatted; + } + $existingPassport->update($updateFields); + } else { + $passportNum = $passportDataInput['passport_number'] ?? $passportDataInput['number'] ?? ('P' . rand(1000000, 9999999)); + $expiryDateFormatted = $expiryDateFormatted ?: now()->addYears(8)->toDateString(); + $issueDateFormatted = $issueDateFormatted ?: now()->subYears(3)->toDateString(); + + $worker->documents()->create([ + 'type' => 'passport', + 'number' => $passportNum, + 'issue_date' => $issueDateFormatted, + 'expiry_date' => $expiryDateFormatted, + 'ocr_accuracy' => 99.0, + 'file_path' => null, + 'ocr_data' => $passportDataInput, + ]); + } + } + } + + // Process visa document creation/update + if ($request->has('visa')) { + $visaDataInput = $request->input('visa'); + if ($visaDataInput) { + $existingVisa = $worker->documents()->where('type', 'visa')->first(); + $expiryDate = $visaDataInput['expiry_date'] ?? null; + $issueDate = $visaDataInput['issue_date'] ?? null; + $expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : null; + $issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : null; + + if ($existingVisa) { + $updateFields = [ + 'ocr_data' => $visaDataInput, + ]; + if ($expiryDateFormatted) { + $updateFields['expiry_date'] = $expiryDateFormatted; + } + if ($issueDateFormatted) { + $updateFields['issue_date'] = $issueDateFormatted; + } + $existingVisa->update($updateFields); + } else { + $visaNum = $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999)); + $expiryDateFormatted = $expiryDateFormatted ?: now()->addYears(2)->toDateString(); + $issueDateFormatted = $issueDateFormatted ?: now()->subYears(2)->toDateString(); + + $worker->documents()->create([ + 'type' => 'visa', + 'number' => $visaNum, + 'issue_date' => $issueDateFormatted, + 'expiry_date' => $expiryDateFormatted, + 'ocr_accuracy' => 99.0, + 'file_path' => null, + 'ocr_data' => $visaDataInput, + ]); + } + } + } }); $worker->load(['skills', 'documents']); + $worker->makeHidden(['email', 'religion', 'availability', 'bio', 'country', 'city', 'area']); + return response()->json([ 'success' => true, 'message' => 'Profile updated successfully.', @@ -552,4 +668,35 @@ public function getDashboard(Request $request) ], 500); } } + + /** + * E.g. "14/05/1990" -> "1990-05-14" + */ + private function normaliseDateForController(?string $raw): ?string + { + if (empty($raw)) { + return null; + } + $raw = trim($raw); + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw)) { + return $raw; + } + try { + if (preg_match('/^(\d{1,2})[\/\- ]([A-Za-z0-9]+)[\/\- ](\d{4})$/', $raw, $m)) { + $day = str_pad($m[1], 2, '0', STR_PAD_LEFT); + $month = $m[2]; + $year = $m[3]; + if (is_numeric($month)) { + $month = str_pad($month, 2, '0', STR_PAD_LEFT); + return "{$year}-{$month}-{$day}"; + } + $ts = strtotime("{$day} {$month} {$year}"); + return $ts ? date('Y-m-d', $ts) : $raw; + } + $dt = new \DateTime($raw); + return $dt->format('Y-m-d'); + } catch (\Exception $e) { + return $raw; + } + } } diff --git a/public/swagger.json b/public/swagger.json index 68b5a94..01413da 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -1,5529 +1,5534 @@ { - "openapi": "3.0.0", - "info": { - "title": "Migrant Mobile API", - "description": "Comprehensive API endpoints for the Migrant Mobile Application, covering worker registration, password login, token authentication, profile management, and interactive job offer responses.", - "version": "1.0.0" - }, - "servers": [ + "openapi": "3.0.0", + "info": { + "title": "Migrant Mobile API", + "description": "Comprehensive API endpoints for the Migrant Mobile Application, covering worker registration, password login, token authentication, profile management, and interactive job offer responses.", + "version": "1.0.0" + }, + "servers": [ + { + "url": "/api", + "description": "Local development API prefix" + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "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": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "full_name", + "mobile", + "password", + "organization_name", + "email", + "nationality", + "city", + "address", + "country_code" + ], + "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": { + "type": "object", + "description": "Optional JSON object containing extracted organization/trade license details.", + "properties": { + "license_number": { + "type": "string", + "example": "123456" + }, + "organization_name": { + "type": "string", + "example": "Al-Rashidi Charitable Foundation" + }, + "expiry_date": { + "type": "string", + "example": "2028-06-12" + } + } + }, + "emirates_id": { + "type": "object", + "description": "Optional JSON object containing extracted Emirates ID details.", + "properties": { + "emirates_id_number": { + "type": "string", + "example": "784-1988-5310327-2" + }, + "name": { + "type": "string", + "example": "KRISHNA PRASAD" + }, + "nationality": { + "type": "string", + "example": "NPL" + }, + "date_of_birth": { + "type": "string", + "example": "1988-03-22" + }, + "expiry_date": { + "type": "string", + "example": "2028-04-11" + }, + "issue_date": { + "type": "string", + "example": "2023-04-11" + }, + "employer": { + "type": "string", + "example": "Federal Authority" + }, + "issue_place": { + "type": "string", + "example": "Abu Dhabi" + }, + "occupation": { + "type": "string", + "example": "Manager" + } + } + }, + "organization_name": { + "type": "string", + "example": "Al-Rashidi Charitable Foundation", + "description": "Name of the sponsoring organization. (REQUIRED)" + }, + "email": { + "type": "string", + "format": "email", + "example": "info@alrashidi.ae", + "description": "Email address (must be unique). (REQUIRED)" + }, + "nationality": { + "type": "string", + "example": "UAE", + "description": "Nationality of the sponsor. (REQUIRED)" + }, + "city": { + "type": "string", + "example": "Dubai", + "description": "City of residence. (REQUIRED)" + }, + "address": { + "type": "string", + "example": "Villa 12, Jumeirah 2", + "description": "Physical address. (REQUIRED)" + }, + "country_code": { + "type": "string", + "example": "+971", + "description": "Country dial code. (REQUIRED)" + }, + "license_expiry": { + "type": "string", + "format": "date", + "example": "2028-06-12", + "description": "Expiry date of the license (YYYY-MM-DD). (REQUIRED)" + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example", + "description": "Firebase Cloud Messaging token for push notifications." + } + } + } + } + } + }, + "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/register/license": { + "post": { + "tags": [ + "Sponsor/Auth" + ], + "summary": "Upload Sponsor Trade License", + "description": "Uploads and processes the trade license document of a Sponsor using OCR.", + "security": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "email", + "license_file" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "info@alrashidi.ae", + "description": "Email address of the registered sponsor. (REQUIRED)" + }, + "license_file": { + "type": "string", + "format": "binary", + "description": "Trade license image/PDF file. (REQUIRED)" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Trade license processed successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "License uploaded and processed successfully." + }, + "ocr_extracted_data": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "expiry_date": { + "type": "string", + "example": "2028-06-12" + }, + "license_number": { + "type": "string", + "example": "123456" + }, + "organization_name": { + "type": "string", + "example": "Charity Co" + } + } + }, + "email": { + "type": "string", + "example": "info@alrashidi.ae" + } + } + } + } + } + }, + "404": { + "description": "Sponsor account not found." + }, + "422": { + "description": "Validation error." + } + } + } + }, + "/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" + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example", + "description": "Firebase Cloud Messaging token for push notifications." + } + } + } + } + } + }, + "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, latest charity events, and total & active counts for employers and workers.", + "responses": { + "200": { + "description": "Dashboard data retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "sponsor": { + "type": "object" + }, + "recent_charity_events": { + "type": "array", + "items": { + "type": "object" + } + }, + "total_events": { + "type": "integer" + }, + "employer_stats": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "example": 12 + }, + "active": { + "type": "integer", + "example": 8 + } + } + }, + "worker_stats": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "example": 150 + }, + "active": { + "type": "integer", + "example": 120 + } + } + } + } + } + } + } + } + } + }, + "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": [ { - "url": "/api", - "description": "Local development API prefix" + "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')." } ], - "security": [ - { - "bearerAuth": [ - - ] - } - ], - "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": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "full_name", - "mobile", - "password", - "organization_name", - "email", - "nationality", - "city", - "address", - "country_code" - ], - "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": { - "type": "object", - "description": "Optional JSON object containing extracted organization/trade license details.", - "properties": { - "license_number": { - "type": "string", - "example": "123456" - }, - "organization_name": { - "type": "string", - "example": "Al-Rashidi Charitable Foundation" - }, - "expiry_date": { - "type": "string", - "example": "2028-06-12" - } - } - }, - "emirates_id": { - "type": "object", - "description": "Optional JSON object containing extracted Emirates ID details.", - "properties": { - "emirates_id_number": { - "type": "string", - "example": "784-1988-5310327-2" - }, - "name": { - "type": "string", - "example": "KRISHNA PRASAD" - }, - "nationality": { - "type": "string", - "example": "NPL" - }, - "date_of_birth": { - "type": "string", - "example": "1988-03-22" - }, - "expiry_date": { - "type": "string", - "example": "2028-04-11" - }, - "issue_date": { - "type": "string", - "example": "2023-04-11" - }, - "employer": { - "type": "string", - "example": "Federal Authority" - }, - "issue_place": { - "type": "string", - "example": "Abu Dhabi" - }, - "occupation": { - "type": "string", - "example": "Manager" - } - } - }, - "organization_name": { - "type": "string", - "example": "Al-Rashidi Charitable Foundation", - "description": "Name of the sponsoring organization. (REQUIRED)" - }, - "email": { - "type": "string", - "format": "email", - "example": "info@alrashidi.ae", - "description": "Email address (must be unique). (REQUIRED)" - }, - "nationality": { - "type": "string", - "example": "UAE", - "description": "Nationality of the sponsor. (REQUIRED)" - }, - "city": { - "type": "string", - "example": "Dubai", - "description": "City of residence. (REQUIRED)" - }, - "address": { - "type": "string", - "example": "Villa 12, Jumeirah 2", - "description": "Physical address. (REQUIRED)" - }, - "country_code": { - "type": "string", - "example": "+971", - "description": "Country dial code. (REQUIRED)" - }, - "license_expiry": { - "type": "string", - "format": "date", - "example": "2028-06-12", - "description": "Expiry date of the license (YYYY-MM-DD). (REQUIRED)" - }, - "fcm_token": { - "type": "string", - "example": "fcm_token_example", - "description": "Firebase Cloud Messaging token for push notifications." - } - } - } - } - } - }, - "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/register/license": { - "post": { - "tags": [ - "Sponsor/Auth" - ], - "summary": "Upload Sponsor Trade License", - "description": "Uploads and processes the trade license document of a Sponsor using OCR.", - "security": [ - - ], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": [ - "email", - "license_file" - ], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "info@alrashidi.ae", - "description": "Email address of the registered sponsor. (REQUIRED)" - }, - "license_file": { - "type": "string", - "format": "binary", - "description": "Trade license image/PDF file. (REQUIRED)" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Trade license processed successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "License uploaded and processed successfully." - }, - "ocr_extracted_data": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "expiry_date": { - "type": "string", - "example": "2028-06-12" - }, - "license_number": { - "type": "string", - "example": "123456" - }, - "organization_name": { - "type": "string", - "example": "Charity Co" - } - } - }, - "email": { - "type": "string", - "example": "info@alrashidi.ae" - } - } - } - } - } - }, - "404": { - "description": "Sponsor account not found." - }, - "422": { - "description": "Validation error." - } - } - } - }, - "/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\u0027s 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" - }, - "fcm_token": { - "type": "string", - "example": "fcm_token_example", - "description": "Firebase Cloud Messaging token for push notifications." - } - } - } - } - } - }, - "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, latest charity events, and total \u0026 active counts for employers and workers.", - "responses": { - "200": { - "description": "Dashboard data retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "data": { - "type": "object", - "properties": { - "sponsor": { - "type": "object" - }, - "recent_charity_events": { - "type": "array", - "items": { - "type": "object" - } - }, - "total_events": { - "type": "integer" - }, - "employer_stats": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "example": 12 - }, - "active": { - "type": "integer", - "example": 8 - } - } - }, - "worker_stats": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "example": 150 - }, - "active": { - "type": "integer", - "example": 120 - } - } - } - } - } - } - } - } - } - }, - "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. \u0027charity\u0027, \u0027update\u0027)." - } - ], - "responses": { - "200": { - "description": "Events retrieved successfully." - }, - "401": { - "description": "Unauthenticated." - } - } - }, - "post": { - "tags": [ - "Sponsor" - ], - "summary": "Post Charity Event (Sponsor)", - "description": "Creates and publishes a new charity event or community drive for the authenticated sponsor.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "title", - "body", - "event_date", - "start_time", - "end_time", - "provided_items", - "location_details", - "location_pin" - ], - "properties": { - "title": { - "type": "string", - "example": "Free Dental Checkup Drive" - }, - "body": { - "type": "string", - "example": "Emirates Charity is organizing a free dental checkup for all workers this Friday morning." - }, - "type": { - "type": "string", - "enum": [ - "charity", - "info", - "warning", - "success" - ], - "example": "charity" - }, - "event_date": { - "type": "string", - "format": "date", - "example": "2026-06-15" - }, - "start_time": { - "type": "string", - "example": "09:00 AM" - }, - "end_time": { - "type": "string", - "example": "04:00 PM" - }, - "provided_items": { - "type": "string", - "example": "Free Dental screening, cleanings, and wellness kits" - }, - "location_details": { - "type": "string", - "example": "Al Quoz Community Hall, Dubai" - }, - "location_pin": { - "type": "string", - "format": "uri", - "example": "https://maps.app.goo.gl/xyz" - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Charity event posted successfully." - }, - "401": { - "description": "Unauthenticated." - }, - "422": { - "description": "Validation error." - } - } - } - }, - "/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." - } - } + "responses": { + "200": { + "description": "Events retrieved successfully." + }, + "401": { + "description": "Unauthenticated." + } + } + }, + "post": { + "tags": [ + "Sponsor" + ], + "summary": "Post Charity Event (Sponsor)", + "description": "Creates and publishes a new charity event or community drive for the authenticated sponsor.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "title", + "body", + "event_date", + "start_time", + "end_time", + "provided_items", + "location_details", + "location_pin" + ], + "properties": { + "title": { + "type": "string", + "example": "Free Dental Checkup Drive" + }, + "body": { + "type": "string", + "example": "Emirates Charity is organizing a free dental checkup for all workers this Friday morning." + }, + "type": { + "type": "string", + "enum": [ + "charity", + "info", + "warning", + "success" + ], + "example": "charity" + }, + "event_date": { + "type": "string", + "format": "date", + "example": "2026-06-15" + }, + "start_time": { + "type": "string", + "example": "09:00 AM" + }, + "end_time": { + "type": "string", + "example": "04:00 PM" + }, + "provided_items": { + "type": "string", + "example": "Free Dental screening, cleanings, and wellness kits" + }, + "location_details": { + "type": "string", + "example": "Al Quoz Community Hall, Dubai" + }, + "location_pin": { + "type": "string", + "format": "uri", + "example": "https://maps.app.goo.gl/xyz" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Charity event posted successfully." + }, + "401": { + "description": "Unauthenticated." + }, + "422": { + "description": "Validation error." + } + } + } + }, + "/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": [ + "Worker/Auth" + ], + "summary": "Register Worker Account (Step 1)", + "description": "Creates the worker account and returns a secure Bearer token.\n\nDocument upload is a **separate step** after registration:\n- `POST /workers/register/passport` — upload passport (OCR-processed)\n- `POST /workers/register/visa` — upload visa (OCR-processed)\n\nBoth document endpoints require the Bearer token returned here.", + "security": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "name", + "phone", + "salary", + "password" + ], + "properties": { + "name": { + "type": "string", + "example": "Amina Al-Masry", + "description": "Full legal name of the worker. (REQUIRED)" + }, + "phone": { + "type": "string", + "example": "+971509998888", + "description": "Contact mobile phone number (must be unique). (REQUIRED)" + }, + "salary": { + "type": "number", + "format": "float", + "example": 1500, + "description": "Desired minimum monthly salary in AED. (REQUIRED)" + }, + "password": { + "type": "string", + "format": "password", + "example": "securepassword123", + "description": "Secret password for subsequent mobile logins (min 6 chars). (REQUIRED)" + }, + "skills": { + "type": "array", + "items": { + "type": "integer" + }, + "example": [ + 1, + 3 + ], + "description": "Optional array of Skill IDs (can be sent as comma-separated string e.g. '1,3' or json '[1,3]' in multipart)." + }, + "language": { + "type": "string", + "example": "English, Arabic", + "description": "Languages spoken by the worker (e.g. 'English, Arabic, Hindi')." + }, + "nationality": { + "type": "string", + "example": "Egypt", + "description": "Nationality of the worker." + }, + "age": { + "type": "integer", + "example": 28, + "description": "Age of the worker." + }, + "experience": { + "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", + "enum": [ + "live_in", + "live_out" + ], + "example": "live_out", + "description": "Accommodation preference (live_in, live_out)." + }, + "gender": { + "type": "string", + "enum": [ + "male", + "female", + "other" + ], + "example": "male", + "description": "Gender of the worker (male, female, other)." + }, + "preferred_location": { + "type": "string", + "example": "Dubai Marina", + "description": "Preferred work location/area." + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example", + "description": "Firebase Cloud Messaging token for push notifications." + }, + "passport": { + "type": "object", + "description": "Optional JSON object containing extracted passport details.", + "properties": { + "authority": { + "type": "string" + }, + "date_of_birth": { + "type": "string", + "example": "14/05/1990" + }, + "date_of_expiry": { + "type": "string", + "example": "05/07/2022" + }, + "date_of_issue": { + "type": "string", + "example": "05/07/2017" + }, + "document_type": { + "type": "string" + }, + "given_names": { + "type": "string" + }, + "issuing_country": { + "type": "string" + }, + "nationality": { + "type": "string" + }, + "passport_number": { + "type": "string" + }, + "personal_number": { + "type": "string" + }, + "place_of_birth": { + "type": "string" + }, + "sex": { + "type": "string" + }, + "surname": { + "type": "string" + } + } + }, + "visa": { + "type": "object", + "description": "Optional JSON object containing extracted visa details.", + "properties": { + "accompanied_by": { + "type": "string" + }, + "expiry_date": { + "type": "string", + "example": "2024-02-15" + }, + "file_number": { + "type": "string" + }, + "id_number": { + "type": "string" + }, + "issue_date": { + "type": "string", + "example": "2022-02-16" + }, + "name": { + "type": "string" + }, + "passport_number": { + "type": "string" + }, + "place_of_issue": { + "type": "string" + }, + "profession": { + "type": "string" + }, + "sponsor": { + "type": "string" + } + } + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Worker registered successfully. Use the returned token for document upload steps.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Worker registered successfully. Please upload your passport and visa documents." + }, + "data": { + "type": "object", + "properties": { + "worker": { + "$ref": "#/components/schemas/Worker" + }, + "token": { + "type": "string", + "example": "abc123xyz456...secureToken..." + } + } + } + } + } + } + } + }, + "422": { + "description": "Validation error — field validation failed or Passport OCR extraction failed.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Validation error." + }, + "errors": { + "type": "object", + "properties": { + "phone": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "This mobile number is already registered." + ] + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "The name field is required." + ] + } + } + } + } + } + } + } + }, + "500": { + "description": "Server error during registration (e.g. database transaction failure).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "An error occurred during worker registration. Please try again." + }, + "error": { + "type": "string", + "example": "Internal Server Error" + } + } + } + } + } + } + } + } + }, + "/workers/login": { + "post": { + "tags": [ + "Worker/Auth" + ], + "summary": "Authenticate Worker", + "description": "Validates worker credentials (mobile phone number and password) and generates a secure, stateless Bearer token for api access.", + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "phone", + "password" + ], + "properties": { + "phone": { + "type": "string", + "example": "+971509998888", + "description": "Registered mobile phone number." + }, + "password": { + "type": "string", + "format": "password", + "example": "securepassword123", + "description": "Secret account password." + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example", + "description": "Firebase Cloud Messaging token for push notifications." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Worker logged in successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Worker logged in successfully." + }, + "data": { + "type": "object", + "properties": { + "worker": { + "$ref": "#/components/schemas/Worker" + }, + "token": { + "type": "string", + "example": "abc123xyz456...secureToken..." + } + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized access.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Invalid mobile number or password." + } + } + } + } + } + } + } + } + }, + "/workers/ocr/passport-vision": { + "post": { + "tags": [ + "Worker/Auth" + ], + "summary": "Extract Passport Data using Google Cloud Vision API", + "description": "Upload a passport image file to extract OCR data using the Google Cloud Vision API.", + "security": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "passport_file" + ], + "properties": { + "passport_file": { + "type": "string", + "format": "binary", + "description": "Passport image file (jpg/png/jpeg, max 10MB)." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Passport data successfully extracted.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "passport_number": { + "type": "string", + "example": "Z43R34255" + }, + "given_names": { + "type": "string", + "example": "AHMAD" + }, + "date_of_birth": { + "type": "string", + "example": "03/07/1978" + }, + "nationality": { + "type": "string", + "example": "ARE" + }, + "issuing_country": { + "type": "string", + "example": "ARE" + }, + "place_of_birth": { + "type": "string", + "example": "DUBAI" + }, + "date_of_issue": { + "type": "string", + "example": "10/02/2015" + }, + "date_of_expiry": { + "type": "string", + "example": "10/02/2020" + } + } + } + } + } + } + } + }, + "422": { + "description": "Validation error (missing passport_file)." + }, + "400": { + "description": "OCR processing error or failed Google Vision API request." + } + } + } + }, + "/workers/config": { + "get": { + "tags": [ + "Worker/Auth" + ], + "summary": "Get Supported Languages and Config", + "description": "Returns a list of supported regional languages (Hindi, Swahili, Tagalog, Tamil) for helper onboarding.", + "security": [], + "responses": { + "200": { + "description": "Config and supported languages retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "languages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "HI" + }, + "name": { + "type": "string", + "example": "Hindi (हिन्दी)" } + } + } + } + } + } + } + } + } + } + } + }, + "/workers/skills": { + "get": { + "tags": [ + "Worker/Auth" + ], + "summary": "Get Master Skills List", + "description": "Returns a list of all available master skills for helper onboarding profile setup.", + "security": [], + "responses": { + "200": { + "description": "Master skills list retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true }, - "/workers/register": { - "post": { - "tags": [ - "Worker/Auth" - ], - "summary": "Register Worker Account (Step 1)", - "description": "Creates the worker account and returns a secure Bearer token.\n\nDocument upload is a **separate step** after registration:\n- `POST /workers/register/passport` — upload passport (OCR-processed)\n- `POST /workers/register/visa` — upload visa (OCR-processed)\n\nBoth document endpoints require the Bearer token returned here.", - "security": [ - - ], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": [ - "name", - "phone", - "salary", - "password" - ], - "properties": { - "name": { - "type": "string", - "example": "Amina Al-Masry", - "description": "Full legal name of the worker. (REQUIRED)" - }, - "phone": { - "type": "string", - "example": "+971509998888", - "description": "Contact mobile phone number (must be unique). (REQUIRED)" - }, - "salary": { - "type": "number", - "format": "float", - "example": 1500, - "description": "Desired minimum monthly salary in AED. (REQUIRED)" - }, - "password": { - "type": "string", - "format": "password", - "example": "securepassword123", - "description": "Secret password for subsequent mobile logins (min 6 chars). (REQUIRED)" - }, - "skills": { - "type": "array", - "items": { - "type": "integer" - }, - "example": [ - 1, - 3 - ], - "description": "Optional array of Skill IDs (can be sent as comma-separated string e.g. \u00271,3\u0027 or json \u0027[1,3]\u0027 in multipart)." - }, - "language": { - "type": "string", - "example": "English, Arabic", - "description": "Languages spoken by the worker (e.g. \u0027English, Arabic, Hindi\u0027)." - }, - "nationality": { - "type": "string", - "example": "Egypt", - "description": "Nationality of the worker." - }, - "age": { - "type": "integer", - "example": 28, - "description": "Age of the worker." - }, - "experience": { - "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", - "enum": [ - "live_in", - "live_out" - ], - "example": "live_out", - "description": "Accommodation preference (live_in, live_out)." - }, - "gender": { - "type": "string", - "enum": [ - "male", - "female", - "other" - ], - "example": "male", - "description": "Gender of the worker (male, female, other)." - }, - "preferred_location": { - "type": "string", - "example": "Dubai Marina", - "description": "Preferred work location/area." - }, - "fcm_token": { - "type": "string", - "example": "fcm_token_example", - "description": "Firebase Cloud Messaging token for push notifications." - }, - "passport": { - "type": "object", - "description": "Optional JSON object containing extracted passport details.", - "properties": { - "authority": { - "type": "string" - }, - "date_of_birth": { - "type": "string", - "example": "14/05/1990" - }, - "date_of_expiry": { - "type": "string", - "example": "05/07/2022" - }, - "date_of_issue": { - "type": "string", - "example": "05/07/2017" - }, - "document_type": { - "type": "string" - }, - "given_names": { - "type": "string" - }, - "issuing_country": { - "type": "string" - }, - "nationality": { - "type": "string" - }, - "passport_number": { - "type": "string" - }, - "personal_number": { - "type": "string" - }, - "place_of_birth": { - "type": "string" - }, - "sex": { - "type": "string" - }, - "surname": { - "type": "string" - } - } - }, - "visa": { - "type": "object", - "description": "Optional JSON object containing extracted visa details.", - "properties": { - "accompanied_by": { - "type": "string" - }, - "expiry_date": { - "type": "string", - "example": "2024-02-15" - }, - "file_number": { - "type": "string" - }, - "id_number": { - "type": "string" - }, - "issue_date": { - "type": "string", - "example": "2022-02-16" - }, - "name": { - "type": "string" - }, - "passport_number": { - "type": "string" - }, - "place_of_issue": { - "type": "string" - }, - "profession": { - "type": "string" - }, - "sponsor": { - "type": "string" - } - } - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Worker registered successfully. Use the returned token for document upload steps.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Worker registered successfully. Please upload your passport and visa documents." - }, - "data": { - "type": "object", - "properties": { - "worker": { - "$ref": "#/components/schemas/Worker" - }, - "token": { - "type": "string", - "example": "abc123xyz456...secureToken..." - } - } - } - } - } - } - } - }, - "422": { - "description": "Validation error — field validation failed or Passport OCR extraction failed.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": false - }, - "message": { - "type": "string", - "example": "Validation error." - }, - "errors": { - "type": "object", - "properties": { - "phone": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "This mobile number is already registered." - ] - }, - "name": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "The name field is required." - ] - } - } - } - } - } - } - } - }, - "500": { - "description": "Server error during registration (e.g. database transaction failure).", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": false - }, - "message": { - "type": "string", - "example": "An error occurred during worker registration. Please try again." - }, - "error": { - "type": "string", - "example": "Internal Server Error" - } - } - } - } - } - } - } - } + "skills": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "Cooking" + } + } + } + } + } + } + } + } + } + } + } + }, + "/nationalities": { + "get": { + "tags": [ + "General" + ], + "summary": "Get Nationalities List", + "description": "Returns a list of supported nationalities translated to the requested language. Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta).", + "security": [], + "parameters": [ + { + "name": "lang", + "in": "query", + "schema": { + "type": "string", + "default": "en" + }, + "description": "Language locale code (e.g. 'en', 'hindi', 'sawahi', 'taglo', 'tamil')" + }, + { + "name": "Accept-Language", + "in": "header", + "schema": { + "type": "string" + }, + "description": "Fallback header locale (e.g. 'en', 'hi', 'sw', 'tl', 'ta')" + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Search keyword to filter nationalities by name or code" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "Page number for pagination" + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 500 + }, + "description": "Number of nationalities to return per page. Defaults to 500 on the backend to retrieve all nationalities by default." + } + ], + "responses": { + "200": { + "description": "Nationalities list retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true }, - "/workers/login": { - "post": { - "tags": [ - "Worker/Auth" - ], - "summary": "Authenticate Worker", - "description": "Validates worker credentials (mobile phone number and password) and generates a secure, stateless Bearer token for api access.", - "security": [ - - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "phone", - "password" - ], - "properties": { - "phone": { - "type": "string", - "example": "+971509998888", - "description": "Registered mobile phone number." - }, - "password": { - "type": "string", - "format": "password", - "example": "securepassword123", - "description": "Secret account password." - }, - "fcm_token": { - "type": "string", - "example": "fcm_token_example", - "description": "Firebase Cloud Messaging token for push notifications." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Worker logged in successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Worker logged in successfully." - }, - "data": { - "type": "object", - "properties": { - "worker": { - "$ref": "#/components/schemas/Worker" - }, - "token": { - "type": "string", - "example": "abc123xyz456...secureToken..." - } - } - } - } - } - } - } - }, - "401": { - "description": "Unauthorized access.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": false - }, - "message": { - "type": "string", - "example": "Invalid mobile number or password." - } - } - } - } - } - } - } - } - }, - "/workers/ocr/passport-vision": { - "post": { - "tags": [ - "Worker/Auth" - ], - "summary": "Extract Passport Data using Google Cloud Vision API", - "description": "Upload a passport image file to extract OCR data using the Google Cloud Vision API.", - "security": [ - - ], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": [ - "passport_file" - ], - "properties": { - "passport_file": { - "type": "string", - "format": "binary", - "description": "Passport image file (jpg/png/jpeg, max 10MB)." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Passport data successfully extracted.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "data": { - "type": "object", - "properties": { - "passport_number": { - "type": "string", - "example": "Z43R34255" - }, - "given_names": { - "type": "string", - "example": "AHMAD" - }, - "date_of_birth": { - "type": "string", - "example": "03/07/1978" - }, - "nationality": { - "type": "string", - "example": "ARE" - }, - "issuing_country": { - "type": "string", - "example": "ARE" - }, - "place_of_birth": { - "type": "string", - "example": "DUBAI" - }, - "date_of_issue": { - "type": "string", - "example": "10/02/2015" - }, - "date_of_expiry": { - "type": "string", - "example": "10/02/2020" - } - } - } - } - } - } - } - }, - "422": { - "description": "Validation error (missing passport_file)." - }, - "400": { - "description": "OCR processing error or failed Google Vision API request." - } - } - } - }, - "/workers/config": { - "get": { - "tags": [ - "Worker/Auth" - ], - "summary": "Get Supported Languages and Config", - "description": "Returns a list of supported regional languages (Hindi, Swahili, Tagalog, Tamil) for helper onboarding.", - "security": [ - - ], - "responses": { - "200": { - "description": "Config and supported languages retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "languages": { - "type": "array", - "items": { - "type": "object", - "properties": { - "code": { - "type": "string", - "example": "HI" - }, - "name": { - "type": "string", - "example": "Hindi (हिन्दी)" - } - } - } - } - } - } - } - } - } - } - } - }, - "/workers/skills": { - "get": { - "tags": [ - "Worker/Auth" - ], - "summary": "Get Master Skills List", - "description": "Returns a list of all available master skills for helper onboarding profile setup.", - "security": [ - - ], - "responses": { - "200": { - "description": "Master skills list retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "skills": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "example": "Cooking" - } - } - } - } - } - } - } - } - } - } - } - }, - "/nationalities": { - "get": { - "tags": [ - "General" - ], - "summary": "Get Nationalities List", - "description": "Returns a list of supported nationalities translated to the requested language. Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta).", - "security": [ - - ], - "parameters": [ - { - "name": "lang", - "in": "query", - "schema": { - "type": "string", - "default": "en" - }, - "description": "Language locale code (e.g. \u0027en\u0027, \u0027hindi\u0027, \u0027sawahi\u0027, \u0027taglo\u0027, \u0027tamil\u0027)" - }, - { - "name": "Accept-Language", - "in": "header", - "schema": { - "type": "string" - }, - "description": "Fallback header locale (e.g. \u0027en\u0027, \u0027hi\u0027, \u0027sw\u0027, \u0027tl\u0027, \u0027ta\u0027)" - }, - { - "name": "search", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Search keyword to filter nationalities by name or code" - }, - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "default": 1 - }, - "description": "Page number for pagination" - }, - { - "name": "per_page", - "in": "query", - "schema": { - "type": "integer", - "default": 500 - }, - "description": "Number of nationalities to return per page. Defaults to 500 on the backend to retrieve all nationalities by default." - } - ], - "responses": { - "200": { - "description": "Nationalities list retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "data": { - "type": "object", - "properties": { - "nationalities": { - "type": "array", - "items": { - "type": "object", - "properties": { - "code": { - "type": "string", - "example": "IN" - }, - "name": { - "type": "string", - "example": "Indian" - } - } - } - }, - "pagination": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "example": 185 - }, - "per_page": { - "type": "integer", - "example": 15 - }, - "current_page": { - "type": "integer", - "example": 1 - }, - "last_page": { - "type": "integer", - "example": 13 - } - } - } - } - } - } - } - } - } - } - } - } - }, - "/languages": { - "get": { - "tags": [ - "General" - ], - "summary": "Get Languages List", - "description": "Returns a list of supported languages translated to the requested language. Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta).", - "security": [ - - ], - "parameters": [ - { - "name": "lang", - "in": "query", - "schema": { - "type": "string", - "default": "en" - }, - "description": "Language locale code (e.g. \u0027en\u0027, \u0027hindi\u0027, \u0027sawahi\u0027, \u0027taglo\u0027, \u0027tamil\u0027)" - }, - { - "name": "Accept-Language", - "in": "header", - "schema": { - "type": "string" - }, - "description": "Fallback header locale (e.g. \u0027en\u0027, \u0027hi\u0027, \u0027sw\u0027, \u0027tl\u0027, \u0027ta\u0027)" - }, - { - "name": "search", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Search keyword to filter languages by name or code" - }, - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "default": 1 - }, - "description": "Page number for pagination" - }, - { - "name": "per_page", - "in": "query", - "schema": { - "type": "integer", - "default": 500 - }, - "description": "Number of languages to return per page." - } - ], - "responses": { - "200": { - "description": "Languages list retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "data": { - "type": "object", - "properties": { - "languages": { - "type": "array", - "items": { - "type": "object", - "properties": { - "code": { - "type": "string", - "example": "en" - }, - "name": { - "type": "string", - "example": "English" - } - } - } - }, - "pagination": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "example": 22 - }, - "per_page": { - "type": "integer", - "example": 15 - }, - "current_page": { - "type": "integer", - "example": 1 - }, - "last_page": { - "type": "integer", - "example": 2 - } - } - } - } - } - } - } - } - } - } - } - } - }, - "/preferred-locations": { - "get": { - "tags": [ - "General" - ], - "summary": "Get Localized Preferred Locations List", - "description": "Returns a list of preferred locations (Dubai, Abu Dhabi, Oman) translated to the requested language. Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta).", - "security": [ - - ], - "parameters": [ - { - "name": "lang", - "in": "query", - "schema": { - "type": "string", - "default": "en" - }, - "description": "Language locale code (e.g. \u0027en\u0027, \u0027hindi\u0027, \u0027sawahi\u0027, \u0027taglo\u0027, \u0027tamil\u0027)" - }, - { - "name": "Accept-Language", - "in": "header", - "schema": { - "type": "string" - }, - "description": "Fallback header locale (e.g. \u0027en\u0027, \u0027hi\u0027, \u0027sw\u0027, \u0027tl\u0027, \u0027ta\u0027)" - }, - { - "name": "search", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Search keyword to filter locations by name or code" - }, - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "default": 1 - }, - "description": "Page number for pagination" - }, - { - "name": "per_page", - "in": "query", - "schema": { - "type": "integer", - "default": 15 - }, - "description": "Number of locations to return per page" - } - ], - "responses": { - "200": { - "description": "Preferred locations list retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "data": { - "type": "object", - "properties": { - "preferred_locations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "code": { - "type": "string", - "example": "dubai" - }, - "name": { - "type": "string", - "example": "Dubai" - } - } - } - }, - "pagination": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "example": 3 - }, - "per_page": { - "type": "integer", - "example": 15 - }, - "current_page": { - "type": "integer", - "example": 1 - }, - "last_page": { - "type": "integer", - "example": 1 - } - } - } - } - } - } - } - } - } - } - } - } - }, - "/preferred-locations/{location}/areas": { - "get": { - "tags": [ - "General" - ], - "summary": "Get Localized Areas for a Preferred Location (Path Parameter)", - "description": "Returns a list of localized neighborhoods or sub-locations mapped under a specific preferred location (Dubai, Abu Dhabi, Oman) using path routing.", - "security": [ - - ], - "parameters": [ - { - "name": "location", - "in": "path", - "required": true, - "description": "The preferred location name (e.g. dubai, abu dhabi, oman).", - "schema": { - "type": "string" - } - }, - { - "name": "lang", - "in": "query", - "schema": { - "type": "string", - "default": "en" - }, - "description": "Language locale code (e.g. \u0027en\u0027, \u0027hindi\u0027, \u0027sawahi\u0027, \u0027taglo\u0027, \u0027tamil\u0027)" - }, - { - "name": "Accept-Language", - "in": "header", - "schema": { - "type": "string" - }, - "description": "Fallback header locale (e.g. \u0027en\u0027, \u0027hi\u0027, \u0027sw\u0027, \u0027tl\u0027, \u0027ta\u0027)" - }, - { - "name": "search", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Search keyword to filter areas by name or code" - }, - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "default": 1 - }, - "description": "Page number for pagination" - }, - { - "name": "per_page", - "in": "query", - "schema": { - "type": "integer", - "default": 15 - }, - "description": "Number of areas to return per page" - } - ], - "responses": { - "200": { - "description": "Areas retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "data": { - "type": "object", - "properties": { - "location": { - "type": "string", - "example": "dubai" - }, - "areas": { - "type": "array", - "items": { - "type": "object", - "properties": { - "code": { - "type": "string", - "example": "marina" - }, - "name": { - "type": "string", - "example": "Dubai Marina" - } - } - } - }, - "pagination": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "example": 17 - }, - "per_page": { - "type": "integer", - "example": 15 - }, - "current_page": { - "type": "integer", - "example": 1 - }, - "last_page": { - "type": "integer", - "example": 2 - } - } - } - } - } - } - } - } - } - } - } - } + "data": { + "type": "object", + "properties": { + "nationalities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "IN" }, - "/preferred-locations/areas": { - "get": { - "tags": [ - "General" - ], - "summary": "Get Localized Areas for a Preferred Location (Query Parameter)", - "description": "Returns a list of localized neighborhoods or sub-locations mapped under a specific preferred location (Dubai, Abu Dhabi, Oman) using query parameter routing.", - "security": [ - - ], - "parameters": [ - { - "name": "city", - "in": "query", - "required": false, - "description": "The preferred location name (e.g. dubai, abu dhabi, oman).", - "schema": { - "type": "string" - } - }, - { - "name": "location", - "in": "query", - "required": false, - "description": "Alternative key for location name (e.g. dubai, abu dhabi, oman).", - "schema": { - "type": "string" - } - }, - { - "name": "lang", - "in": "query", - "schema": { - "type": "string", - "default": "en" - }, - "description": "Language locale code (e.g. \u0027en\u0027, \u0027hindi\u0027, \u0027sawahi\u0027, \u0027taglo\u0027, \u0027tamil\u0027)" - }, - { - "name": "Accept-Language", - "in": "header", - "schema": { - "type": "string" - }, - "description": "Fallback header locale (e.g. \u0027en\u0027, \u0027hi\u0027, \u0027sw\u0027, \u0027tl\u0027, \u0027ta\u0027)" - }, - { - "name": "search", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Search keyword to filter areas by name or code" - }, - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "default": 1 - }, - "description": "Page number for pagination" - }, - { - "name": "per_page", - "in": "query", - "schema": { - "type": "integer", - "default": 15 - }, - "description": "Number of areas to return per page" - } - ], - "responses": { - "200": { - "description": "Areas retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "data": { - "type": "object", - "properties": { - "location": { - "type": "string", - "example": "dubai" - }, - "areas": { - "type": "array", - "items": { - "type": "object", - "properties": { - "code": { - "type": "string", - "example": "marina" - }, - "name": { - "type": "string", - "example": "Dubai Marina" - } - } - } - }, - "pagination": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "example": 17 - }, - "per_page": { - "type": "integer", - "example": 15 - }, - "current_page": { - "type": "integer", - "example": 1 - }, - "last_page": { - "type": "integer", - "example": 2 - } - } - } - } - } - } - } - } - } - } - } - } - }, - "/workers/send-otp": { - "post": { - "tags": [ - "Worker/Auth" - ], - "summary": "Send Mobile OTP Verification Code", - "description": "Dispatches a mock 6-digit OTP verification code (\u0027111111\u0027) to the worker\u0027s mobile phone number or email address.", - "security": [ - - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "phone" - ], - "properties": { - "phone": { - "type": "string", - "example": "+971501234567", - "description": "Mobile contact phone number or identifier." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OTP dispatched successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Verification code sent. (Dev mock: 111111)" - }, - "identifier": { - "type": "string", - "example": "+971501234567" - } - } - } - } - } - } - } - } - }, - "/workers/verify-otp": { - "post": { - "tags": [ - "Worker/Auth" - ], - "summary": "Verify Mobile OTP Code", - "description": "Validates the received 6-digit OTP code against the cached record. Sets a 1-hour secure gate allowing profile setup.", - "security": [ - - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "phone", - "otp" - ], - "properties": { - "phone": { - "type": "string", - "example": "+971501234567" - }, - "otp": { - "type": "string", - "example": "111111", - "description": "6-digit OTP received by the worker." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OTP verified and registration gate unlocked.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "OTP verified successfully. You may proceed with profile setup." - }, - "identifier": { - "type": "string", - "example": "+971501234567" - } - } - } - } - } - }, - "422": { - "description": "Invalid OTP code or expired session." - } - } - } - }, - "/workers/setup-profile": { - "post": { - "tags": [ - "Worker/Auth" - ], - "summary": "Complete Basic Profile Setup", - "description": "Saves basic onboarding data (Name, Nationality, Language, Skills) and generates the permanent worker account along with a secure bearer access token.", - "security": [ - - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "phone", - "name", - "nationality", - "language" - ], - "properties": { - "phone": { - "type": "string", - "example": "+971501234567" - }, - "name": { - "type": "string", - "example": "Rahul Sharma" - }, - "nationality": { - "type": "string", - "example": "Indian" - }, - "language": { - "type": "string", - "enum": [ - "HI", - "SW", - "TL", - "TA" - ], - "example": "HI" - }, - "skills": { - "type": "array", - "items": { - "type": "integer" - }, - "example": [ - 6, - 8 - ], - "description": "Array of skill IDs" - }, - "gender": { - "type": "string", - "enum": [ - "male", - "female", - "other" - ], - "example": "male", - "description": "Gender of the worker (male, female, other)." - }, - "live_in_out": { - "type": "string", - "enum": [ - "live_in", - "live_out" - ], - "example": "live_out", - "description": "Accommodation preference (live_in, live_out)." - }, - "preferred_location": { - "type": "string", - "example": "Dubai Marina", - "description": "Preferred work location/area." - }, - "fcm_token": { - "type": "string", - "example": "fcm_token_example", - "description": "Firebase Cloud Messaging token for push notifications." - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Account created and profile configured successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Registration complete! Welcome to Migrant." - }, - "data": { - "type": "object", - "properties": { - "worker": { - "$ref": "#/components/schemas/Worker" - }, - "token": { - "type": "string", - "example": "abc123xyz456...secureToken..." - } - } - } - } - } - } - } - } - } - } - }, - "/workers/profile": { - "get": { - "tags": [ - "Worker/Profile" - ], - "summary": "Get Profile details", - "description": "Retrieves the authenticated worker\u0027s profile details including their selected job category, connected skills, and uploaded documents.", - "responses": { - "200": { - "description": "Worker profile details retrieved.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "data": { - "type": "object", - "properties": { - "worker": { - "$ref": "#/components/schemas/Worker" - } - } - } - } - } - } - } - } - } - } - }, - "/workers/profile/update": { - "post": { - "tags": [ - "Worker/Profile" - ], - "summary": "Update Profile details", - "description": "Allows workers to customize and complete their profiles by updating age, nationality, desired salary, bio, availability, experience, religion, categories, and master skills list.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string", - "example": "Amina Al-Masry" - }, - "age": { - "type": "integer", - "example": 28 - }, - "nationality": { - "type": "string", - "example": "Egypt" - }, - "salary": { - "type": "number", - "example": 1800 - }, - "availability": { - "type": "string", - "example": "Immediate" - }, - "experience": { - "type": "string", - "example": "4 Years" - }, - "religion": { - "type": "string", - "example": "Muslim" - }, - "bio": { - "type": "string", - "example": "Highly certified housekeeper and babysitter with cooking experience." - }, - "skills": { - "type": "array", - "items": { - "type": "integer" - }, - "example": [ - 6, - 8 - ] - }, - "fcm_token": { - "type": "string", - "example": "fcm-device-token-xyz-123", - "description": "Firebase Cloud Messaging token for push notifications." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Profile updated successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Profile updated successfully." - }, - "data": { - "type": "object", - "properties": { - "worker": { - "$ref": "#/components/schemas/Worker" - } - } - } - } - } - } - } - } - } - } - }, - "/workers/go-live": { - "post": { - "tags": [ - "Worker/Profile" - ], - "summary": "Profile Go Live", - "description": "Sets the worker status to \u0027active\u0027 and availability to \u0027Immediate\u0027, making the profile searchable on the sponsor/employer marketplace.", - "responses": { - "200": { - "description": "Worker profile is now live and searchable.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Your profile is now live! Status set to Active." - } - } - } - } - } - } - } - } - }, - "/workers/profile/toggle-availability": { - "post": { - "tags": [ - "Worker/Profile" - ], - "summary": "Toggle Profile Search Visibility", - "description": "Updates worker search visibility based on whether they are still actively seeking a job. \u0027still_looking\u0027 as true makes profile active, false hides it.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "still_looking" - ], - "properties": { - "still_looking": { - "type": "boolean", - "example": true, - "description": "Whether the worker is still actively looking for job offers." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Availability toggled successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "still_looking": { - "type": "boolean", - "example": true - }, - "status": { - "type": "string", - "example": "active" - } - } - } - } - } - } - } - } - }, - "/workers/profile/mark-hired": { - "post": { - "tags": [ - "Worker/Profile" - ], - "summary": "Mark Worker as Hired", - "description": "Changes the worker\u0027s status to \u0027Hired\u0027, which automatically removes them from active marketplace searches.", - "responses": { - "200": { - "description": "Worker marked as Hired.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Congratulations! You have been successfully marked as Hired." - } - } - } - } - } - } - } - } - }, - "/workers/offers": { - "get": { - "tags": [ - "Worker/Offers" - ], - "summary": "View Received Job Offers", - "description": "Returns a list of all custom job/hiring offers received by the worker from different employers (e.g. Work Date, location, salaries, and notes).", - "responses": { - "200": { - "description": "List of job offers retrieved.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "data": { - "type": "object", - "properties": { - "offers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/JobOffer" - } - } - } - } - } - } - } - } - } - } - } - }, - "/workers/offers/{id}/respond": { - "post": { - "tags": [ - "Worker/Offers" - ], - "summary": "Accept or Reject Job Offer", - "description": "Responds to a pending job offer. If accepted, the job offer status becomes \u0027accepted\u0027 and the worker\u0027s status is automatically changed to \u0027Hired\u0027, which reflects immediately in the Employer panel.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The Job Offer ID reference.", - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "status" - ], - "properties": { - "status": { - "type": "string", - "enum": [ - "accepted", - "rejected" - ], - "example": "accepted", - "description": "Response action (accepted or rejected)." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Job offer response saved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Job offer successfully accepted." - }, - "data": { - "type": "object", - "properties": { - "offer": { - "$ref": "#/components/schemas/JobOffer" - }, - "worker_status": { - "type": "string", - "example": "Hired" - } - } - } - } - } - } - } - } - } - } - }, - "/workers/conversations": { - "get": { - "tags": [ - "Worker/Conversations" - ], - "summary": "Get Conversations List (Worker)", - "description": "Retrieves all active conversations for the authenticated worker.", - "responses": { - "200": { - "description": "Conversations list retrieved successfully." - } - } - } - }, - "/workers/conversations/{id}/messages": { - "get": { - "tags": [ - "Worker/Conversations" - ], - "summary": "Get Conversation Messages (Worker)", - "description": "Retrieves message history for a conversation and marks incoming employer messages as read.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Messages retrieved successfully." - } - } - }, - "post": { - "tags": [ - "Worker/Conversations" - ], - "summary": "Send Reply Message (Worker)", - "description": "Sends a reply message from the worker to the employer.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "text": { - "type": "string", - "example": "Yes, I am available.", - "description": "Optional text message content (required if no file is uploaded)." - }, - "file": { - "type": "string", - "format": "binary", - "description": "Optional file or voice note attachment (Max 10MB). Supports images, documents, and common audio formats." - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Message sent successfully." - } - } - } - }, - "/workers/report": { - "post": { - "tags": [ - "Worker/Profile" - ], - "summary": "Submit a Safety/Moderation Report (Worker)", - "description": "Submits a report on a chat conversation or a review. The report is logged in the safety queue and reviewed by administrators.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "type", - "item_id", - "reason" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "Chat", - "Review" - ], - "example": "Chat", - "description": "The type of entity being reported (Chat or Review)." - }, - "item_id": { - "type": "string", - "example": "1", - "description": "The ID of the conversation or review being reported." - }, - "user_id": { - "type": "string", - "example": "5", - "description": "Optional ID of the user being reported." - }, - "reason": { - "type": "string", - "example": "Others", - "description": "The reason for submitting this report." - }, - "others": { - "type": "string", - "example": "Spam and malicious links", - "description": "Custom reason string. Used only if reason is \u0027Others\u0027." - }, - "description": { - "type": "string", - "example": "The other party sent highly inappropriate and insulting messages.", - "description": "Optional detailed explanation/comments." - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Report submitted successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Report submitted successfully. Our safety team will review it shortly." - }, - "report_id": { - "type": "string", - "example": "REP-ABCD1234" - } - } - } - } - } - }, - "404": { - "description": "Conversation or Review not found, or user is not authorized to report it." - }, - "422": { - "description": "Validation error." - } - } - } - }, - "/workers/report-reasons": { - "get": { - "tags": [ - "Worker/Profile" - ], - "summary": "Get Safety Report Reasons (Worker)", - "description": "Returns a list of active safety/moderation report reasons. Can filter reasons by target type (Chat or Review).", - "parameters": [ - { - "name": "type", - "in": "query", - "required": false, - "description": "Optional type to filter reasons (Chat or Review). If not provided, returns all active reasons.", - "schema": { - "type": "string", - "enum": [ - "Chat", - "Review" - ] - } - } - ], - "responses": { - "200": { - "description": "List of report reasons retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "reasons": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "reason": { - "type": "string", - "example": "Abusive language / Profanity" - }, - "type": { - "type": "string", - "example": "Both" - } - } - } - } - } - } - } - } - } - } - } - }, - "/workers/tickets": { - "get": { - "tags": [ - "Worker/Support" - ], - "summary": "List Support Tickets (Worker)", - "description": "Retrieves a paginated list of all support tickets created by the authenticated worker.", - "responses": { - "200": { - "description": "Tickets retrieved successfully." - } - } - }, - "post": { - "tags": [ - "Worker/Support" - ], - "summary": "Create Support Ticket (Worker)", - "description": "Creates a support ticket for a worker with a subject, description, and optional priority level.", - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": [ - "subject", - "description" - ], - "properties": { - "subject": { - "type": "string", - "example": "App login crashes on launch" - }, - "description": { - "type": "string", - "example": "When opening the app on my phone, it crashes immediately on the splash screen." - }, - "priority": { - "type": "string", - "enum": [ - "low", - "medium", - "high" - ], - "example": "medium" - }, - "reason_id": { - "type": "integer", - "example": 1, - "description": "Optional report/support reason ID." - }, - "voice_note": { - "type": "string", - "format": "binary", - "description": "Optional audio/voice note file attachment." - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Ticket created successfully." - } - } - } - }, - "/workers/tickets/{id}": { - "get": { - "tags": [ - "Worker/Support" - ], - "summary": "Get Support Ticket Details \u0026 Replies (Worker)", - "description": "Retrieves the details of a support ticket and its chronological reply history.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Ticket details and replies retrieved successfully." - } - } + "name": { + "type": "string", + "example": "Indian" + } } + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "example": 185 + }, + "per_page": { + "type": "integer", + "example": 15 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "last_page": { + "type": "integer", + "example": 13 + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/languages": { + "get": { + "tags": [ + "General" + ], + "summary": "Get Languages List", + "description": "Returns a list of supported languages translated to the requested language. Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta).", + "security": [], + "parameters": [ + { + "name": "lang", + "in": "query", + "schema": { + "type": "string", + "default": "en" + }, + "description": "Language locale code (e.g. 'en', 'hindi', 'sawahi', 'taglo', 'tamil')" + }, + { + "name": "Accept-Language", + "in": "header", + "schema": { + "type": "string" + }, + "description": "Fallback header locale (e.g. 'en', 'hi', 'sw', 'tl', 'ta')" + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Search keyword to filter languages by name or code" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "Page number for pagination" + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 500 + }, + "description": "Number of languages to return per page." + } + ], + "responses": { + "200": { + "description": "Languages list retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "languages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "en" + }, + "name": { + "type": "string", + "example": "English" + } + } + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "example": 22 + }, + "per_page": { + "type": "integer", + "example": 15 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "last_page": { + "type": "integer", + "example": 2 + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/preferred-locations": { + "get": { + "tags": [ + "General" + ], + "summary": "Get Localized Preferred Locations List", + "description": "Returns a list of preferred locations (Dubai, Abu Dhabi, Oman) translated to the requested language. Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta).", + "security": [], + "parameters": [ + { + "name": "lang", + "in": "query", + "schema": { + "type": "string", + "default": "en" + }, + "description": "Language locale code (e.g. 'en', 'hindi', 'sawahi', 'taglo', 'tamil')" + }, + { + "name": "Accept-Language", + "in": "header", + "schema": { + "type": "string" + }, + "description": "Fallback header locale (e.g. 'en', 'hi', 'sw', 'tl', 'ta')" + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Search keyword to filter locations by name or code" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "Page number for pagination" + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 15 + }, + "description": "Number of locations to return per page" + } + ], + "responses": { + "200": { + "description": "Preferred locations list retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "preferred_locations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "dubai" + }, + "name": { + "type": "string", + "example": "Dubai" + } + } + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "example": 3 + }, + "per_page": { + "type": "integer", + "example": 15 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "last_page": { + "type": "integer", + "example": 1 + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/preferred-locations/{location}/areas": { + "get": { + "tags": [ + "General" + ], + "summary": "Get Localized Areas for a Preferred Location (Path Parameter)", + "description": "Returns a list of localized neighborhoods or sub-locations mapped under a specific preferred location (Dubai, Abu Dhabi, Oman) using path routing.", + "security": [], + "parameters": [ + { + "name": "location", + "in": "path", + "required": true, + "description": "The preferred location name (e.g. dubai, abu dhabi, oman).", + "schema": { + "type": "string" + } + }, + { + "name": "lang", + "in": "query", + "schema": { + "type": "string", + "default": "en" + }, + "description": "Language locale code (e.g. 'en', 'hindi', 'sawahi', 'taglo', 'tamil')" + }, + { + "name": "Accept-Language", + "in": "header", + "schema": { + "type": "string" + }, + "description": "Fallback header locale (e.g. 'en', 'hi', 'sw', 'tl', 'ta')" + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Search keyword to filter areas by name or code" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "Page number for pagination" + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 15 + }, + "description": "Number of areas to return per page" + } + ], + "responses": { + "200": { + "description": "Areas retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "location": { + "type": "string", + "example": "dubai" + }, + "areas": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "marina" + }, + "name": { + "type": "string", + "example": "Dubai Marina" + } + } + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "example": 17 + }, + "per_page": { + "type": "integer", + "example": 15 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "last_page": { + "type": "integer", + "example": 2 + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/preferred-locations/areas": { + "get": { + "tags": [ + "General" + ], + "summary": "Get Localized Areas for a Preferred Location (Query Parameter)", + "description": "Returns a list of localized neighborhoods or sub-locations mapped under a specific preferred location (Dubai, Abu Dhabi, Oman) using query parameter routing.", + "security": [], + "parameters": [ + { + "name": "city", + "in": "query", + "required": false, + "description": "The preferred location name (e.g. dubai, abu dhabi, oman).", + "schema": { + "type": "string" + } + }, + { + "name": "location", + "in": "query", + "required": false, + "description": "Alternative key for location name (e.g. dubai, abu dhabi, oman).", + "schema": { + "type": "string" + } + }, + { + "name": "lang", + "in": "query", + "schema": { + "type": "string", + "default": "en" + }, + "description": "Language locale code (e.g. 'en', 'hindi', 'sawahi', 'taglo', 'tamil')" + }, + { + "name": "Accept-Language", + "in": "header", + "schema": { + "type": "string" + }, + "description": "Fallback header locale (e.g. 'en', 'hi', 'sw', 'tl', 'ta')" + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Search keyword to filter areas by name or code" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "Page number for pagination" + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 15 + }, + "description": "Number of areas to return per page" + } + ], + "responses": { + "200": { + "description": "Areas retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "location": { + "type": "string", + "example": "dubai" + }, + "areas": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "marina" + }, + "name": { + "type": "string", + "example": "Dubai Marina" + } + } + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "example": 17 + }, + "per_page": { + "type": "integer", + "example": 15 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "last_page": { + "type": "integer", + "example": 2 + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/workers/send-otp": { + "post": { + "tags": [ + "Worker/Auth" + ], + "summary": "Send Mobile OTP Verification Code", + "description": "Dispatches a mock 6-digit OTP verification code ('111111') to the worker's mobile phone number or email address.", + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "phone" + ], + "properties": { + "phone": { + "type": "string", + "example": "+971501234567", + "description": "Mobile contact phone number or identifier." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OTP dispatched successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Verification code sent. (Dev mock: 111111)" + }, + "identifier": { + "type": "string", + "example": "+971501234567" + } + } + } + } + } + } + } + } + }, + "/workers/verify-otp": { + "post": { + "tags": [ + "Worker/Auth" + ], + "summary": "Verify Mobile OTP Code", + "description": "Validates the received 6-digit OTP code against the cached record. Sets a 1-hour secure gate allowing profile setup.", + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "phone", + "otp" + ], + "properties": { + "phone": { + "type": "string", + "example": "+971501234567" + }, + "otp": { + "type": "string", + "example": "111111", + "description": "6-digit OTP received by the worker." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OTP verified and registration gate unlocked.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "OTP verified successfully. You may proceed with profile setup." + }, + "identifier": { + "type": "string", + "example": "+971501234567" + } + } + } + } + } + }, + "422": { + "description": "Invalid OTP code or expired session." + } + } + } + }, + "/workers/setup-profile": { + "post": { + "tags": [ + "Worker/Auth" + ], + "summary": "Complete Basic Profile Setup", + "description": "Saves basic onboarding data (Name, Nationality, Language, Skills) and generates the permanent worker account along with a secure bearer access token.", + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "phone", + "name", + "nationality", + "language" + ], + "properties": { + "phone": { + "type": "string", + "example": "+971501234567" + }, + "name": { + "type": "string", + "example": "Rahul Sharma" + }, + "nationality": { + "type": "string", + "example": "Indian" + }, + "language": { + "type": "string", + "enum": [ + "HI", + "SW", + "TL", + "TA" + ], + "example": "HI" + }, + "skills": { + "type": "array", + "items": { + "type": "integer" + }, + "example": [ + 6, + 8 + ], + "description": "Array of skill IDs" + }, + "gender": { + "type": "string", + "enum": [ + "male", + "female", + "other" + ], + "example": "male", + "description": "Gender of the worker (male, female, other)." + }, + "live_in_out": { + "type": "string", + "enum": [ + "live_in", + "live_out" + ], + "example": "live_out", + "description": "Accommodation preference (live_in, live_out)." + }, + "preferred_location": { + "type": "string", + "example": "Dubai Marina", + "description": "Preferred work location/area." + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example", + "description": "Firebase Cloud Messaging token for push notifications." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Account created and profile configured successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Registration complete! Welcome to Migrant." + }, + "data": { + "type": "object", + "properties": { + "worker": { + "$ref": "#/components/schemas/Worker" + }, + "token": { + "type": "string", + "example": "abc123xyz456...secureToken..." + } + } + } + } + } + } + } + } + } + } + }, + "/workers/profile": { + "get": { + "tags": [ + "Worker/Profile" + ], + "summary": "Get Profile details", + "description": "Retrieves the authenticated worker's profile details including their selected job category, connected skills, and uploaded documents.", + "responses": { + "200": { + "description": "Worker profile details retrieved.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "worker": { + "$ref": "#/components/schemas/Worker" + } + } + } + } + } + } + } + } + } + } + }, + "/workers/profile/update": { + "post": { + "tags": [ + "Worker/Profile" + ], + "summary": "Update Profile details", + "description": "Allows workers to customize and complete their profiles by updating name, phone, age, nationality, desired salary, language, preferred job type, live in/out accommodation preference, preferred location, experience, skills list, fcm_token, passport details, and visa details.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Amina Al-Masry" + }, + "phone": { + "type": "string", + "example": "+971509998888" + }, + "age": { + "type": "integer", + "example": 28 + }, + "nationality": { + "type": "string", + "example": "Egypt" + }, + "language": { + "type": "string", + "enum": [ + "HI", + "SW", + "TL", + "TA" + ], + "example": "HI" + }, + "salary": { + "type": "number", + "example": 1800 + }, + "preferred_job_type": { + "type": "string", + "example": "full-time", + "description": "Preferred job type (e.g., full-time, part-time)." + }, + "live_in_out": { + "type": "string", + "enum": [ + "live_in", + "live_out" + ], + "example": "live_out", + "description": "Accommodation preference (live_in, live_out)." + }, + "preferred_location": { + "type": "string", + "example": "Dubai Marina", + "description": "Preferred work location/area." + }, + "experience": { + "type": "string", + "example": "4 Years" + }, + "skills": { + "type": "array", + "items": { + "type": "integer" + }, + "example": [ + 6, + 8 + ] + }, + "fcm_token": { + "type": "string", + "example": "fcm-device-token-xyz-123", + "description": "Firebase Cloud Messaging token for push notifications." + }, + "passport": { + "type": "object", + "properties": { + "passport_number": { + "type": "string", + "example": "ABC123456" }, - "/workers/tickets/{id}/reply": { - "post": { - "tags": [ - "Worker/Support" - ], - "summary": "Reply to Support Ticket (Worker)", - "description": "Submits a text reply or message to an active, open, or resolved support ticket.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "example": "I tried clearing cache and it still crashes." - }, - "voice_note": { - "type": "string", - "format": "binary", - "description": "Optional audio/voice note file attachment." - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Reply posted successfully." - } - } - } - }, - "/employers/register": { - "post": { - "tags": [ - "Employer/Auth" - ], - "summary": "Register Sponsor (Alias)", - "description": "Legacy route matching the simplified sponsor registration (Name, Email, Phone) (Step 1).", - "security": [ - - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "name", - "email", - "phone" - ], - "properties": { - "name": { - "type": "string", - "example": "Ahmad Bin Ahmed", - "description": "Sponsor\u0027s full name" - }, - "email": { - "type": "string", - "example": "ahmad@example.com", - "description": "Sponsor\u0027s contact email address" - }, - "phone": { - "type": "string", - "example": "+971509990001", - "description": "Sponsor\u0027s mobile phone number" - }, - "emirates_id": { - "type": "object", - "description": "Optional JSON object containing extracted Emirates ID details.", - "properties": { - "emirates_id_number": { - "type": "string", - "example": "784-1988-5310327-2" - }, - "name": { - "type": "string", - "example": "Ahmad Bin Ahmed" - }, - "date_of_birth": { - "type": "string", - "example": "1990-01-01" - }, - "nationality": { - "type": "string", - "example": "Emirati" - }, - "issue_date": { - "type": "string", - "example": "2023-04-11" - }, - "expiry_date": { - "type": "string", - "example": "2028-04-11" - }, - "employer": { - "type": "string", - "example": "Federal Authority" - }, - "issue_place": { - "type": "string", - "example": "Abu Dhabi" - }, - "occupation": { - "type": "string", - "example": "Manager" - } - } - }, - "fcm_token": { - "type": "string", - "example": "fcm_token_example", - "description": "Firebase Cloud Messaging token for push notifications." - }, - "address": { - "type": "string", - "example": "Villa 45, Street 12", - "description": "Physical address/location details." - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Sponsor registered successfully." - } - } - } - }, - "/employers/verify": { - "post": { - "tags": [ - "Employer/Auth" - ], - "summary": "Verify Sponsor Email OTP", - "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" - }, - "fcm_token": { - "type": "string", - "example": "fcm_token_example", - "description": "Firebase Cloud Messaging token for push notifications." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Email verified successfully." - } - } - } - }, - "/employers/payment": { - "post": { - "tags": [ - "Employer/Auth" - ], - "summary": "Sponsor Subscription Payment", - "description": "Submits a successful plan selection and PayTabs payment confirmation (Step 4).", - "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": [ - "Employer/Auth" - ], - "summary": "Create Sponsor Account Password", - "description": "Configures and finalizes the portal login password to complete registration (Step 5). 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" - }, - "fcm_token": { - "type": "string", - "example": "fcm_token_example", - "description": "Firebase Cloud Messaging token for push notifications." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Password set and registration finalized successfully." - } - } - } - }, - "/employers/plans": { - "get": { - "tags": [ - "Employer/Auth" - ], - "summary": "Get Available Subscription Plans", - "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 or Sponsor", - "description": "Unified authentication endpoint. Validates email/mobile and password, then returns a stateless Bearer token and the user\u0027s role (employer or sponsor).", - "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" - }, - "fcm_token": { - "type": "string", - "example": "fcm_token_example", - "description": "Firebase Cloud Messaging token for push notifications." - } - } - } - } - } - }, - "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": "employer", - "enum": [ - "employer", - "sponsor" - ] - }, - "data": { - "type": "object", - "properties": { - "token": { - "type": "string", - "example": "api_token_here" - } - } - } - } - } - } - } - } - } - } - }, - "/employers/conversations": { - "get": { - "tags": [ - "Employer/Conversations" - ], - "summary": "Get Conversations List (Employer)", - "description": "Retrieves all active candidate conversations for the authenticated employer.", - "responses": { - "200": { - "description": "Conversations list retrieved successfully." - } - } - } - }, - "/employers/conversations/{id}/messages": { - "get": { - "tags": [ - "Employer/Conversations" - ], - "summary": "Get Conversation Messages (Employer)", - "description": "Retrieves message history for a conversation and marks incoming worker messages as read.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Messages retrieved successfully." - } - } - }, - "post": { - "tags": [ - "Employer/Conversations" - ], - "summary": "Send Reply Message (Employer)", - "description": "Sends a reply message from the employer to the worker.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "text": { - "type": "string", - "example": "When can you start?", - "description": "Optional text message content (required if no file is uploaded)." - }, - "file": { - "type": "string", - "format": "binary", - "description": "Optional file or voice note attachment (Max 10MB). Supports images, documents, and common audio formats." - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Message sent successfully." - } - } - } - }, - "/employers/conversations/start": { - "post": { - "tags": [ - "Employer/Conversations" - ], - "summary": "Start Conversation with Worker", - "description": "Starts or retrieves a conversation with the specified worker ID.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "worker_id" - ], - "properties": { - "worker_id": { - "type": "integer", - "example": 1 - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Conversation started successfully." - } - } - } - }, - "/workers/announcements": { - "get": { - "tags": [ - "Worker/CharityEvents" - ], - "summary": "Get Charity Events (Worker)", - "description": "Allows workers to retrieve the list of active charity events, food drives, and medical support programs scheduled in Dubai.", - "parameters": [ - { - "name": "page", - "in": "query", - "required": false, - "description": "Page number.", - "schema": { - "type": "integer", - "default": 1 - } - }, - { - "name": "per_page", - "in": "query", - "required": false, - "description": "Number of items per page.", - "schema": { - "type": "integer", - "default": 15 - } - } - ], - "responses": { - "200": { - "description": "List of charity events retrieved successfully." - } - } - } - }, - "/workers/announcements/new": { - "get": { - "tags": [ - "Worker/CharityEvents" - ], - "summary": "Get New/Unviewed Announcements/Charity Events", - "description": "Retrieves up to 3 active approved charity events/announcements that the worker has not yet viewed, and automatically marks them as viewed.", - "responses": { - "200": { - "description": "Successfully retrieved new announcements.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 4 - }, - "title": { - "type": "string", - "example": "Free Dental Checkup Camp" - }, - "body": { - "type": "string", - "example": "Emirates Charity is providing free screening." - }, - "type": { - "type": "string", - "example": "charity" - }, - "employer_name": { - "type": "string", - "example": "Emirates Charity" - }, - "company_name": { - "type": "string", - "example": "Emirates Charity Foundation" - }, - "created_at": { - "type": "string", - "format": "date-time", - "example": "2026-06-01T10:00:00.000000Z" - }, - "time_ago": { - "type": "string", - "example": "5 hours ago" - } - } - } - } - } - } - } - } - }, - "401": { - "description": "Unauthenticated." - } - } - } - }, - "/workers/announcements/{id}/view": { - "post": { - "tags": [ - "Worker/CharityEvents" - ], - "summary": "Mark Specific Announcement as Viewed", - "description": "Explicitly marks a specific announcement/charity event as viewed by the authenticated worker.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "Announcement ID", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Announcement marked as viewed successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Announcement marked as viewed." - } - } - } - } - } - }, - "401": { - "description": "Unauthenticated." - }, - "404": { - "description": "Announcement not found." - } - } - } - }, - "/employers/announcements": { - "get": { - "tags": [ - "Employer/CharityEvents" - ], - "summary": "Get Posted Charity Events (Employer)", - "description": "Allows employers/sponsors to retrieve the list of charity events they have published.", - "parameters": [ - { - "name": "page", - "in": "query", - "required": false, - "description": "Page number.", - "schema": { - "type": "integer", - "default": 1 - } - }, - { - "name": "per_page", - "in": "query", - "required": false, - "description": "Number of items per page.", - "schema": { - "type": "integer", - "default": 15 - } - } - ], - "responses": { - "200": { - "description": "List of posted charity events retrieved successfully." - } - } - }, - "post": { - "tags": [ - "Employer/CharityEvents" - ], - "summary": "Publish Charity Event (Employer)", - "description": "Allows employers/sponsors to publish a new community charity event or drive to all workers in Dubai, triggering instant push notifications and morning-of reminders.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "title", - "content", - "provided_items", - "event_date", - "event_time", - "location_details", - "location_pin" - ], - "properties": { - "title": { - "type": "string", - "example": "Free Dental Checkup Camp" - }, - "content": { - "type": "string", - "example": "Emirates Charity is providing free professional dental screening, cleanings, and educational packages for domestic helpers." - }, - "provided_items": { - "type": "string", - "example": "Free Dental screening, cleanings, and wellness kits" - }, - "event_date": { - "type": "string", - "format": "date", - "example": "2026-06-15" - }, - "event_time": { - "type": "string", - "example": "9:00 AM - 4:00 PM" - }, - "location_details": { - "type": "string", - "example": "Al Quoz Community Hall, Dubai" - }, - "location_pin": { - "type": "string", - "format": "uri", - "example": "https://maps.app.goo.gl/xyz" - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Charity event created and notifications scheduled successfully." - } - } - } - }, - "/employers/announcements/{id}": { - "delete": { - "tags": [ - "Employer/CharityEvents" - ], - "summary": "Delete Charity Event (Employer)", - "description": "Allows employers/sponsors to delete a previously published charity event.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Charity event deleted successfully." - } - } - } - }, - "/employers/dashboard": { - "get": { - "tags": [ - "Employer/Profile" - ], - "summary": "Get Employer Dashboard stats \u0026 recent events", - "description": "Retrieves stats including contacted workers count, total hired workers, saved candidates, current plan details, and recent announcements or events.", - "responses": { - "200": { - "description": "Employer dashboard stats retrieved successfully." - } - } - } - }, - "/employers/profile": { - "get": { - "tags": [ - "Employer/Profile" - ], - "summary": "Get Profile details (Employer)", - "description": "Retrieves the authenticated employer\u0027s profile details including their selected company name, phone, language preference, and notification settings.", - "responses": { - "200": { - "description": "Employer profile details retrieved successfully." - } - } - } - }, - "/employers/profile/update": { - "post": { - "tags": [ - "Employer/Profile" - ], - "summary": "Update Profile details (Employer)", - "description": "Allows employers to update their profile details (company name, phone number, name, email, language preference, and notification settings) and change their account password.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "name", - "email", - "phone", - "company_name", - "language", - "notifications" - ], - "properties": { - "name": { - "type": "string", - "example": "Ahmad" - }, - "email": { - "type": "string", - "example": "ahmad@example.com" - }, - "phone": { - "type": "string", - "example": "+971509990001" - }, - "company_name": { - "type": "string", - "example": "Ahmad Tech Ltd" - }, - "language": { - "type": "string", - "enum": [ - "English", - "Arabic", - "english", - "arabic" - ], - "example": "English" - }, - "notifications": { - "type": "boolean", - "example": true - }, - "current_password": { - "type": "string", - "example": "Password@123" - }, - "new_password": { - "type": "string", - "example": "NewSecurePassword@123" - }, - "new_password_confirmation": { - "type": "string", - "example": "NewSecurePassword@123" - }, - "fcm_token": { - "type": "string", - "example": "fcm_token_example", - "description": "Firebase Cloud Messaging token for push notifications." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Profile updated successfully." - } - } - } - }, - "/employers/workers": { - "get": { - "tags": [ - "Employer/Pipeline" - ], - "summary": "Get Available Workers", - "description": "Returns a list of verified workers that are currently available to be hired.", - "parameters": [ - { - "name": "search", - "in": "query", - "required": false, - "description": "Search term matching helper name, nationality, or religion.", - "schema": { - "type": "string" - } - }, - { - "name": "skills", - "in": "query", - "required": false, - "description": "Filter by skills (comma-separated list).", - "schema": { - "type": "string" - } - }, - { - "name": "language", - "in": "query", - "required": false, - "description": "Filter by language.", - "schema": { - "type": "string" - } - }, - { - "name": "nationality", - "in": "query", - "required": false, - "description": "Filter by nationality name (comma-separated list for multiple choice, e.g. Indian,Kenyan,Ethiopian).", - "schema": { - "type": "string" - } - }, - { - "name": "gender", - "in": "query", - "required": false, - "description": "Filter by gender (e.g. male, female).", - "schema": { - "type": "string" - } - }, - { - "name": "preferred_location", - "in": "query", - "required": false, - "description": "Filter by preferred location (comma-separated list, e.g. Dubai,Abu Dhabi).", - "schema": { - "type": "string" - } - }, - { - "name": "area", - "in": "query", - "required": false, - "description": "Filter by preferred area (comma-separated list, e.g. Marina,Al Barsha).", - "schema": { - "type": "string" - } - }, - { - "name": "job_type", - "in": "query", - "required": false, - "description": "Filter by job type (comma-separated list, e.g. full-time,part-time).", - "schema": { - "type": "string" - } - }, - { - "name": "accommodation_type", - "in": "query", - "required": false, - "description": "Filter by accommodation type (comma-separated list, e.g. live_in,live_out).", - "schema": { - "type": "string" - } - }, - { - "name": "in_country", - "in": "query", - "required": false, - "description": "Filter by in country status (true/false or yes/no or in/out).", - "schema": { - "type": "string" - } - }, - { - "name": "out_country", - "in": "query", - "required": false, - "description": "Filter by out country status (true/false or yes/no or out/in).", - "schema": { - "type": "string" - } - }, - { - "name": "visa_status", - "in": "query", - "required": false, - "description": "Filter by next visa status (comma-separated list, e.g. Residence Visa,Tourist Visa). Only applicable if in country.", - "schema": { - "type": "string" - } - }, - { - "name": "availability", - "in": "query", - "required": false, - "description": "Filter by availability status.", - "schema": { - "type": "string" - } - }, - { - "name": "page", - "in": "query", - "required": false, - "description": "Page number.", - "schema": { - "type": "integer", - "default": 1 - } - }, - { - "name": "per_page", - "in": "query", - "required": false, - "description": "Number of items per page.", - "schema": { - "type": "integer", - "default": 15 - } - } - ], - "responses": { - "200": { - "description": "Available workers list retrieved successfully." - } - } - } - }, - "/employers/workers/{id}": { - "get": { - "tags": [ - "Employer/Pipeline" - ], - "summary": "Get Worker Profile Detail", - "description": "Retrieves the complete profile details for a worker, mirroring the web detailed view, along with active conversation details if already started with this employer.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "Worker ID reference.", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Worker profile details retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "data": { - "type": "object", - "properties": { - "worker": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "example": "Jane Doe" - }, - "nationality": { - "type": "string", - "example": "Filipino" - }, - "photo": { - "type": "string", - "example": "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format\u0026fit=crop\u0026q=80\u0026w=200" - }, - "emirates_id_status": { - "type": "string", - "example": "Passport Verified", - "description": "Deprecated - Use passport_status instead" - }, - "passport_status": { - "type": "string", - "example": "Passport Verified" - }, - "category": { - "type": "string", - "example": "Domestic Worker" - }, - "skills": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "childcare", - "cooking" - ] - }, - "availability_status": { - "type": "string", - "example": "Active" - }, - "visa_status": { - "type": "string", - "example": "Residence Visa" - }, - "experience": { - "type": "string", - "example": "5 Years" - }, - "experience_years": { - "type": "integer", - "example": 5 - }, - "salary": { - "type": "integer", - "example": 2500 - }, - "religion": { - "type": "string", - "example": "Christian" - }, - "languages": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "English", - "Tagalog" - ] - }, - "age": { - "type": "integer", - "example": 28 - }, - "verified": { - "type": "boolean", - "example": true - }, - "preferred_job_type": { - "type": "string", - "example": "live-in" - }, - "bio": { - "type": "string", - "example": "Experienced and caring domestic worker specialing in childcare and housekeeping..." - }, - "rating": { - "type": "number", - "example": 4.3 - }, - "reviews_count": { - "type": "integer", - "example": 6 - }, - "reviews": { - "type": "array", - "items": { - "type": "object" - } - }, - "similar_workers": { - "type": "array", - "items": { - "type": "object" - } - }, - "conversation_id": { - "type": "integer", - "nullable": true, - "example": 4 - } - } - } - } - } - } - } - } - } - }, - "404": { - "description": "Worker profile not found." - } - } - } - }, - "/employers/candidates": { - "get": { - "tags": [ - "Employer/Pipeline" - ], - "summary": "Get Candidate Applications and Offers", - "description": "Returns all job applications and direct hiring offers connected to the authenticated sponsor account that have achieved \u0027Hired\u0027 status.", - "parameters": [ - { - "name": "search", - "in": "query", - "required": false, - "description": "Search term matching helper name, nationality, or religion.", - "schema": { - "type": "string" - } - }, - { - "name": "skills", - "in": "query", - "required": false, - "description": "Filter candidates by skills (comma-separated list of skill names, e.g. cooking,driving).", - "schema": { - "type": "string" - } - }, - { - "name": "languages", - "in": "query", - "required": false, - "description": "Filter candidates by languages spoken (comma-separated list of language names, e.g. English,Arabic).", - "schema": { - "type": "string" - } - }, - { - "name": "nationality", - "in": "query", - "required": false, - "description": "Filter candidates by nationality name (comma-separated list for multiple choice, e.g. Indian,Kenyan,Ethiopian).", - "schema": { - "type": "string" - } - }, - { - "name": "gender", - "in": "query", - "required": false, - "description": "Filter candidates by gender (e.g. male, female).", - "schema": { - "type": "string" - } - }, - { - "name": "preferred_location", - "in": "query", - "required": false, - "description": "Filter candidates by preferred location (comma-separated list, e.g. Dubai,Abu Dhabi).", - "schema": { - "type": "string" - } - }, - { - "name": "job_type", - "in": "query", - "required": false, - "description": "Filter candidates by job type (comma-separated list, e.g. full-time,part-time).", - "schema": { - "type": "string" - } - }, - { - "name": "accommodation_type", - "in": "query", - "required": false, - "description": "Filter candidates by accommodation type (comma-separated list, e.g. live_in,live_out).", - "schema": { - "type": "string" - } - }, - { - "name": "in_country", - "in": "query", - "required": false, - "description": "Filter candidates by in country status (true/false or yes/no or in/out).", - "schema": { - "type": "string" - } - }, - { - "name": "out_country", - "in": "query", - "required": false, - "description": "Filter candidates by out country status (true/false or yes/no or out/in).", - "schema": { - "type": "string" - } - }, - { - "name": "visa_status", - "in": "query", - "required": false, - "description": "Filter candidates by next visa status (comma-separated list, e.g. Residence Visa,Tourist Visa). Only applicable if in country.", - "schema": { - "type": "string" - } - }, - { - "name": "availability", - "in": "query", - "required": false, - "description": "Filter candidates by availability status.", - "schema": { - "type": "string" - } - }, - { - "name": "page", - "in": "query", - "required": false, - "description": "Page number.", - "schema": { - "type": "integer", - "default": 1 - } - }, - { - "name": "per_page", - "in": "query", - "required": false, - "description": "Number of items per page.", - "schema": { - "type": "integer", - "default": 15 - } - } - ], - "responses": { - "200": { - "description": "Employer candidate pipeline retrieved successfully." - } - } - } + "date_of_expiry": { + "type": "string", + "example": "2030-01-01" }, - "/employers/candidates/{id}/hire": { - "post": { - "tags": [ - "Employer/Pipeline" - ], - "summary": "Mark Candidate as Hired", - "description": "Transitions the target candidate application, job offer, or helper status to \u0027Hired\u0027.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "The target identifier (Application ID, Offer ID, or Worker ID).", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Candidate successfully status updated to Hired." - } - } - } - }, - "/employers/shortlist": { - "get": { - "tags": [ - "Employer/Pipeline" - ], - "summary": "Get Shortlisted Workers", - "description": "Returns a list of all workers shortlisted by the authenticated sponsor.", - "parameters": [ - { - "name": "page", - "in": "query", - "required": false, - "description": "Page number.", - "schema": { - "type": "integer", - "default": 1 - } - }, - { - "name": "per_page", - "in": "query", - "required": false, - "description": "Number of items per page.", - "schema": { - "type": "integer", - "default": 15 - } - } - ], - "responses": { - "200": { - "description": "Shortlisted workers retrieved successfully." - } - } - }, - "post": { - "tags": [ - "Employer/Pipeline" - ], - "summary": "Toggle Worker Shortlist Status", - "description": "Toggles (adds or removes) a worker to/from the authenticated sponsor\u0027s shortlist.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "worker_id" - ], - "properties": { - "worker_id": { - "type": "integer", - "example": 1, - "description": "Worker ID to shortlist/unshortlist." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Shortlist status toggled successfully." - } - } - } - }, - "/employers/payments": { - "get": { - "tags": [ - "Employer/Billing" - ], - "summary": "Get Payment History", - "description": "Retrieves the complete transaction history for the authenticated sponsor/employer, including subscription passes and document verification receipts.", - "parameters": [ - { - "name": "page", - "in": "query", - "required": false, - "description": "Page number.", - "schema": { - "type": "integer", - "default": 1 - } - }, - { - "name": "per_page", - "in": "query", - "required": false, - "description": "Number of items per page.", - "schema": { - "type": "integer", - "default": 15 - } - } - ], - "responses": { - "200": { - "description": "Employer billing history retrieved successfully." - } - } - } - }, - "/workers/dashboard/views": { - "get": { - "tags": [ - "Worker/Dashboard" - ], - "summary": "Get Worker Profile Views Statistics", - "description": "Allows authenticated workers to see the total count and detailed list of employers/sponsors who viewed their full profile.", - "responses": { - "200": { - "description": "Profile views statistics retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "data": { - "type": "object", - "properties": { - "views_count": { - "type": "integer", - "example": 5 - }, - "views_list": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "employer_id": { - "type": "integer", - "example": 2 - }, - "employer_name": { - "type": "string", - "example": "Ahmad" - }, - "company_name": { - "type": "string", - "example": "Ahmad Tech Ltd" - }, - "nationality": { - "type": "string", - "example": "UAE" - }, - "city": { - "type": "string", - "example": "Dubai" - }, - "viewed_at": { - "type": "string", - "format": "date-time", - "example": "2026-06-01T15:10:24.000000Z" - }, - "viewed_at_formatted": { - "type": "string", - "example": "2 minutes ago" - } - } - } - } - } - } - } - } - } - } - } - } - } - }, - "/workers/dashboard": { - "get": { - "tags": [ - "Worker/Dashboard" - ], - "summary": "Get Worker Consolidated Dashboard Data", - "description": "Allows authenticated workers to retrieve their dashboard metrics: active status, profile views count, currently working sponsor/employer details, and latest charity events/announcements.", - "responses": { - "200": { - "description": "Worker dashboard details retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "data": { - "type": "object", - "properties": { - "active_status": { - "type": "string", - "example": "active" - }, - "count_sponsors_viewed": { - "type": "integer", - "example": 5 - }, - "employer_contacted": { - "type": "integer", - "example": 2, - "description": "Unique count of employers who contacted this worker." - }, - "profile_viewed": { - "type": "integer", - "example": 3, - "description": "Unique count of employers who viewed this worker\u0027s profile." - }, - "currently_working_sponsor": { - "type": "object", - "nullable": true, - "properties": { - "id": { - "type": "integer", - "example": 2 - }, - "name": { - "type": "string", - "example": "Ahmad" - }, - "company_name": { - "type": "string", - "example": "Ahmad Tech Ltd" - }, - "nationality": { - "type": "string", - "example": "UAE" - }, - "city": { - "type": "string", - "example": "Dubai" - }, - "email": { - "type": "string", - "example": "ahmad@example.com" - }, - "phone": { - "type": "string", - "example": "+971509990001" - }, - "hired_at": { - "type": "string", - "format": "date-time", - "example": "2026-06-01T15:10:24.000000Z" - }, - "hired_at_formatted": { - "type": "string", - "example": "Jun 01, 2026" - } - } - }, - "latest_charity_events_list": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "title": { - "type": "string", - "example": "Free Dental Checkup Camp" - }, - "body": { - "type": "string", - "example": "Emirates Charity is providing free screening." - }, - "type": { - "type": "string", - "example": "charity" - }, - "employer_name": { - "type": "string", - "example": "Emirates Charity" - }, - "company_name": { - "type": "string", - "example": "Emirates Charity Foundation" - }, - "created_at": { - "type": "string", - "format": "date-time", - "example": "2026-06-01T10:00:00.000000Z" - }, - "time_ago": { - "type": "string", - "example": "5 hours ago" - } - } - } - } - } - } - } - } - } - } - } - } - } - }, - "/employers/reviews": { - "get": { - "tags": [ - "Employer/Reviews" - ], - "summary": "Get Reviews Written by Employer", - "description": "Retrieves a paginated list of all reviews written by the authenticated employer.", - "parameters": [ - { - "name": "page", - "in": "query", - "required": false, - "description": "Page number.", - "schema": { - "type": "integer", - "default": 1 - } - }, - { - "name": "per_page", - "in": "query", - "required": false, - "description": "Number of items per page.", - "schema": { - "type": "integer", - "default": 15 - } - } - ], - "responses": { - "200": { - "description": "Reviews retrieved successfully." - } - } - }, - "post": { - "tags": [ - "Employer/Reviews" - ], - "summary": "Add or Update Worker Review", - "description": "Allows employers to submit a review score and description comment for a worker. If the employer has already left a review, this will update it.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "worker_id", - "rating" - ], - "properties": { - "worker_id": { - "type": "integer", - "example": 136, - "description": "Worker ID being reviewed." - }, - "rating": { - "type": "integer", - "minimum": 1, - "maximum": 5, - "example": 5, - "description": "Review score from 1 to 5." - }, - "comment": { - "type": "string", - "example": "Highly professional, punctual and did a great job!", - "description": "Optional detailed comment." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Review updated successfully." - }, - "201": { - "description": "Review added successfully." - }, - "422": { - "description": "Validation error." - } - } - } - }, - "/employers/reviews/{id}": { - "put": { - "tags": [ - "Employer/Reviews" - ], - "summary": "Edit Worker Review", - "description": "Allows employers to edit their previously created review score and comment by review ID.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "Review ID reference.", - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "rating" - ], - "properties": { - "rating": { - "type": "integer", - "minimum": 1, - "maximum": 5, - "example": 4, - "description": "Updated rating score from 1 to 5." - }, - "comment": { - "type": "string", - "example": "Updated feedback comment.", - "description": "Updated review comment description." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Review edited successfully." - }, - "404": { - "description": "Review not found or unauthorized." - }, - "422": { - "description": "Validation error." - } - } - } - }, - "/employers/report": { - "post": { - "tags": [ - "Employer/Reviews" - ], - "summary": "Submit a Safety/Moderation Report (Employer)", - "description": "Submits a report on a chat conversation or a review. The report is logged in the safety queue and reviewed by administrators.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "type", - "item_id", - "reason" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "Chat", - "Review" - ], - "example": "Chat", - "description": "The type of entity being reported (Chat or Review)." - }, - "item_id": { - "type": "string", - "example": "1", - "description": "The ID of the conversation or review being reported." - }, - "user_id": { - "type": "string", - "example": "5", - "description": "Optional ID of the user being reported." - }, - "reason": { - "type": "string", - "example": "Others", - "description": "The reason for submitting this report." - }, - "others": { - "type": "string", - "example": "Spam and malicious links", - "description": "Custom reason string. Used only if reason is \u0027Others\u0027." - }, - "description": { - "type": "string", - "example": "The worker requested payments off-platform and sent abusive messages.", - "description": "Optional detailed explanation/comments." - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Report submitted successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Report submitted successfully. Our safety team will review it shortly." - }, - "report_id": { - "type": "string", - "example": "REP-ABCD1234" - } - } - } - } - } - }, - "404": { - "description": "Conversation or Review not found, or user is not authorized to report it." - }, - "422": { - "description": "Validation error." - } - } - } + "date_of_issue": { + "type": "string", + "example": "2020-01-01" + } + } + }, + "visa": { + "type": "object", + "properties": { + "file_number": { + "type": "string", + "example": "202/2022/2840234" + }, + "expiry_date": { + "type": "string", + "example": "2028-12-31" + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Profile updated successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true }, - "/employers/report-reasons": { - "get": { - "tags": [ - "Employer/Reviews" - ], - "summary": "Get Safety Report Reasons (Employer)", - "description": "Returns a list of active safety/moderation report reasons. Can filter reasons by target type (Chat or Review).", - "parameters": [ - { - "name": "type", - "in": "query", - "required": false, - "description": "Optional type to filter reasons (Chat or Review). If not provided, returns all active reasons.", - "schema": { - "type": "string", - "enum": [ - "Chat", - "Review" - ] - } - } - ], - "responses": { - "200": { - "description": "List of report reasons retrieved successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "reasons": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "reason": { - "type": "string", - "example": "Abusive language / Profanity" - }, - "type": { - "type": "string", - "example": "Both" - } - } - } - } - } - } - } - } - } - } - } - }, - "/employers/tickets": { - "get": { - "tags": [ - "Employer/Support" - ], - "summary": "List Support Tickets (Employer)", - "description": "Retrieves a paginated list of all support tickets created by the authenticated employer.", - "responses": { - "200": { - "description": "Tickets retrieved successfully." - } - } - }, - "post": { - "tags": [ - "Employer/Support" - ], - "summary": "Create Support Ticket (Employer)", - "description": "Creates a support ticket for an employer with a subject, description, and optional priority level.", - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": [ - "subject", - "description" - ], - "properties": { - "subject": { - "type": "string", - "example": "Payment card rejected" - }, - "description": { - "type": "string", - "example": "My Visa card was declined when trying to buy the Premium Employer Pass." - }, - "priority": { - "type": "string", - "enum": [ - "low", - "medium", - "high" - ], - "example": "medium" - }, - "reason_id": { - "type": "integer", - "example": 1, - "description": "Optional report/support reason ID." - }, - "voice_note": { - "type": "string", - "format": "binary", - "description": "Optional audio/voice note file attachment." - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Ticket created successfully." - } - } - } - }, - "/employers/tickets/{id}": { - "get": { - "tags": [ - "Employer/Support" - ], - "summary": "Get Support Ticket Details \u0026 Replies (Employer)", - "description": "Retrieves the details of a support ticket and its chronological reply history.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Ticket details and replies retrieved successfully." - } - } - } - }, - "/employers/tickets/{id}/reply": { - "post": { - "tags": [ - "Employer/Support" - ], - "summary": "Reply to Support Ticket (Employer)", - "description": "Submits a text reply or message to an active, open, or resolved support ticket.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "example": "I tried a different card and it still fails." - }, - "voice_note": { - "type": "string", - "format": "binary", - "description": "Optional audio/voice note file attachment." - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Reply posted successfully." - } - } - } - }, - "/workers/forgot-password": { - "post": { - "tags": [ - "Workers - Auth" - ], - "summary": "Forgot Password (Send OTP)", - "description": "Sends a 6-digit OTP to verify the worker\u0027s identity using their registered mobile number. Workers do not use email — mobile/phone is the primary identifier. OTP is valid for 10 minutes.", - "operationId": "workerForgotPassword", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "phone" - ], - "properties": { - "phone": { - "type": "string", - "example": "+971501234567", - "description": "Worker\u0027s registered mobile number" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OTP sent successfully (response is identical whether account exists or not, to prevent enumeration)", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "If an account with that mobile number exists, a reset OTP has been sent." - } - } - } - } - } - }, - "422": { - "description": "Validation error — phone is required" - } - } - } - }, - "/workers/reset-password": { - "post": { - "tags": [ - "Workers - Auth" - ], - "summary": "Reset Password (Verify OTP)", - "description": "Verifies the OTP sent to the worker\u0027s mobile number and sets a new password. The phone number must match the one used in the forgot-password step.", - "operationId": "workerResetPassword", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "phone", - "otp", - "password", - "password_confirmation" - ], - "properties": { - "phone": { - "type": "string", - "example": "+971501234567", - "description": "Worker\u0027s registered mobile number" - }, - "otp": { - "type": "string", - "example": "482910", - "description": "6-digit OTP received" - }, - "password": { - "type": "string", - "example": "newpassword123", - "description": "New password (min 8 chars)" - }, - "password_confirmation": { - "type": "string", - "example": "newpassword123", - "description": "Must match password" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Password reset successfully" - }, - "422": { - "description": "Invalid OTP, expired OTP, or validation error" - }, - "404": { - "description": "Account not found" - } - } - } - }, - "/employers/forgot-password": { - "post": { - "tags": [ - "Employers - Auth" - ], - "summary": "Forgot Password (Send OTP)", - "description": "Sends a 6-digit OTP to the employer\u0027s registered email address. OTP expires in 10 minutes.", - "operationId": "employerForgotPassword", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "email" - ], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "employer@example.com" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OTP sent (response identical whether account exists or not)" - }, - "422": { - "description": "Validation error" - } - } - } - }, - "/employers/reset-password": { - "post": { - "tags": [ - "Employers - Auth" - ], - "summary": "Reset Password (Verify OTP)", - "description": "Verifies the OTP sent to the employer\u0027s email and sets a new password.", - "operationId": "employerResetPassword", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "email", - "otp", - "password", - "password_confirmation" - ], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "employer@example.com" - }, - "otp": { - "type": "string", - "example": "482910" - }, - "password": { - "type": "string", - "example": "newpassword123" - }, - "password_confirmation": { - "type": "string", - "example": "newpassword123" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Password reset successfully" - }, - "422": { - "description": "Invalid or expired OTP" - } - } - } - }, - "/sponsors/forgot-password": { - "post": { - "tags": [ - "Sponsors - Auth" - ], - "summary": "Forgot Password (Send OTP)", - "description": "Sends a 6-digit OTP to the sponsor\u0027s registered email. Accepts email or mobile number as identifier. OTP expires in 10 minutes.", - "operationId": "sponsorForgotPassword", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "sponsor@example.com", - "description": "Email address (either email or mobile required)" - }, - "mobile": { - "type": "string", - "example": "+971501234567", - "description": "Mobile number (either email or mobile required)" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OTP sent (response identical whether account exists or not)" - }, - "422": { - "description": "Validation error — email or mobile required" - } - } - } - }, - "/sponsors/reset-password": { - "post": { - "tags": [ - "Sponsors - Auth" - ], - "summary": "Reset Password (Verify OTP)", - "description": "Verifies the OTP sent to the sponsor\u0027s email and sets a new password. Use the email field to identify the account.", - "operationId": "sponsorResetPassword", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "email", - "otp", - "password", - "password_confirmation" - ], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "sponsor@example.com" - }, - "otp": { - "type": "string", - "example": "482910" - }, - "password": { - "type": "string", - "example": "newpassword123" - }, - "password_confirmation": { - "type": "string", - "example": "newpassword123" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Password reset successfully" - }, - "422": { - "description": "Invalid or expired OTP" - } - } - } - } - }, - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" - } - }, - "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" - }, - "emirates_id_file": { - "type": "string", - "example": "uploads/licenses/1234567890_emirates_id.pdf" - }, - "fcm_token": { - "type": "string", - "example": "fcm_token_example" - }, - "created_at": { - "type": "string", - "format": "date-time" - } - } - }, - "Worker": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 136 - }, - "name": { - "type": "string", - "example": "Amina Al-Masry" - }, - "email": { - "type": "string", - "example": "worker.971509998888@migrant.com" - }, - "phone": { - "type": "string", - "example": "+971509998888" - }, - "language": { - "type": "string", - "example": "HI" - }, - "fcm_token": { - "type": "string", - "example": "fcm_token_example" - }, - "nationality": { - "type": "string", - "example": "Egypt" - }, - "age": { - "type": "integer", - "example": 28 - }, - "salary": { - "type": "string", - "example": "1500.00" - }, - "availability": { - "type": "string", - "example": "Immediate" - }, - "experience": { - "type": "string", - "example": "4 Years" - }, - "religion": { - "type": "string", - "example": "Muslim" - }, - "bio": { - "type": "string", - "example": "Certified infant caregiver and housekeeper with excellent cooking skills." - }, - "verified": { - "type": "boolean", - "example": false - }, - "status": { - "type": "string", - "example": "active" - }, - "in_country": { - "type": "boolean", - "example": true - }, - "visa_status": { - "type": "string", - "example": "Tourist Visa" - }, - "passport_status": { - "type": "string", - "example": "Passport Verified" - }, - "emirates_id_status": { - "type": "string", - "example": "Passport Verified" - }, - "preferred_job_type": { - "type": "string", - "example": "full-time" - }, - "live_in_out": { - "type": "string", - "example": "Live-out" - }, - "gender": { - "type": "string", - "enum": [ - "male", - "female", - "other" - ], - "example": "male" - }, - "preferred_location": { - "type": "string", - "example": "Dubai Marina" - }, - "country": { - "type": "string", - "example": "UAE" - }, - "city": { - "type": "string", - "example": "Dubai" - }, - "area": { - "type": "string", - "example": "Marina" - }, - "created_at": { - "type": "string", - "format": "date-time", - "example": "2026-05-20T14:54:26.000000Z" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "example": "2026-05-20T14:54:26.000000Z" - }, - "category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 7 - }, - "name": { - "type": "string", - "example": "General Helper" - } - } - }, - "skills": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 6 - }, - "name": { - "type": "string", - "example": "Cleaning" - } - } - } - }, - "documents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WorkerDocument" - } - } - } - }, - "WorkerDocument": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 15 - }, - "worker_id": { - "type": "integer", - "example": 136 - }, - "type": { - "type": "string", - "example": "passport" - }, - "number": { - "type": "string", - "example": "P345892" - }, - "issue_date": { - "type": "string", - "format": "date", - "example": "2024-05-20" - }, - "expiry_date": { - "type": "string", - "format": "date", - "example": "2034-05-20" - }, - "ocr_accuracy": { - "type": "number", - "example": 98.5 - }, - "file_path": { - "type": "string", - "example": "uploads/documents/1716200000_passport_my_file.jpg" - } - } - }, - "JobOffer": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "employer_id": { - "type": "integer", - "example": 2 - }, - "worker_id": { - "type": "integer", - "example": 136 - }, - "work_date": { - "type": "string", - "format": "date", - "example": "2026-06-01" - }, - "location": { - "type": "string", - "example": "Dubai Marina, Silverene Towers" - }, - "salary": { - "type": "string", - "example": "2500.00" - }, - "notes": { - "type": "string", - "example": "Require housekeeping support starting from June 1st." - }, - "status": { - "type": "string", - "example": "pending" - }, - "created_at": { - "type": "string", - "format": "date-time", - "example": "2026-05-20T15:23:44.000000Z" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "example": "2026-05-20T15:23:44.000000Z" - } - } + "message": { + "type": "string", + "example": "Profile updated successfully." + }, + "data": { + "type": "object", + "properties": { + "worker": { + "$ref": "#/components/schemas/Worker" + } + } + } + } + } + } + } + } + } + } + }, + "/workers/go-live": { + "post": { + "tags": [ + "Worker/Profile" + ], + "summary": "Profile Go Live", + "description": "Sets the worker status to 'active' and availability to 'Immediate', making the profile searchable on the sponsor/employer marketplace.", + "responses": { + "200": { + "description": "Worker profile is now live and searchable.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Your profile is now live! Status set to Active." + } + } + } + } + } + } + } + } + }, + "/workers/profile/toggle-availability": { + "post": { + "tags": [ + "Worker/Profile" + ], + "summary": "Toggle Profile Search Visibility", + "description": "Updates worker search visibility based on whether they are still actively seeking a job. 'still_looking' as true makes profile active, false hides it.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "still_looking" + ], + "properties": { + "still_looking": { + "type": "boolean", + "example": true, + "description": "Whether the worker is still actively looking for job offers." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Availability toggled successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "still_looking": { + "type": "boolean", + "example": true + }, + "status": { + "type": "string", + "example": "active" + } + } + } + } + } + } + } + } + }, + "/workers/profile/mark-hired": { + "post": { + "tags": [ + "Worker/Profile" + ], + "summary": "Mark Worker as Hired", + "description": "Changes the worker's status to 'Hired', which automatically removes them from active marketplace searches.", + "responses": { + "200": { + "description": "Worker marked as Hired.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Congratulations! You have been successfully marked as Hired." + } + } + } + } + } + } + } + } + }, + "/workers/offers": { + "get": { + "tags": [ + "Worker/Offers" + ], + "summary": "View Received Job Offers", + "description": "Returns a list of all custom job/hiring offers received by the worker from different employers (e.g. Work Date, location, salaries, and notes).", + "responses": { + "200": { + "description": "List of job offers retrieved.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "offers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/JobOffer" } - } - } -} + } + } + } + } + } + } + } + } + } + } + }, + "/workers/offers/{id}/respond": { + "post": { + "tags": [ + "Worker/Offers" + ], + "summary": "Accept or Reject Job Offer", + "description": "Responds to a pending job offer. If accepted, the job offer status becomes 'accepted' and the worker's status is automatically changed to 'Hired', which reflects immediately in the Employer panel.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The Job Offer ID reference.", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "example": "accepted", + "description": "Response action (accepted or rejected)." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Job offer response saved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Job offer successfully accepted." + }, + "data": { + "type": "object", + "properties": { + "offer": { + "$ref": "#/components/schemas/JobOffer" + }, + "worker_status": { + "type": "string", + "example": "Hired" + } + } + } + } + } + } + } + } + } + } + }, + "/workers/conversations": { + "get": { + "tags": [ + "Worker/Conversations" + ], + "summary": "Get Conversations List (Worker)", + "description": "Retrieves all active conversations for the authenticated worker.", + "responses": { + "200": { + "description": "Conversations list retrieved successfully." + } + } + } + }, + "/workers/conversations/{id}/messages": { + "get": { + "tags": [ + "Worker/Conversations" + ], + "summary": "Get Conversation Messages (Worker)", + "description": "Retrieves message history for a conversation and marks incoming employer messages as read.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Messages retrieved successfully." + } + } + }, + "post": { + "tags": [ + "Worker/Conversations" + ], + "summary": "Send Reply Message (Worker)", + "description": "Sends a reply message from the worker to the employer.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "example": "Yes, I am available.", + "description": "Optional text message content (required if no file is uploaded)." + }, + "file": { + "type": "string", + "format": "binary", + "description": "Optional file or voice note attachment (Max 10MB). Supports images, documents, and common audio formats." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Message sent successfully." + } + } + } + }, + "/workers/report": { + "post": { + "tags": [ + "Worker/Profile" + ], + "summary": "Submit a Safety/Moderation Report (Worker)", + "description": "Submits a report on a chat conversation or a review. The report is logged in the safety queue and reviewed by administrators.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "type", + "item_id", + "reason" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Chat", + "Review" + ], + "example": "Chat", + "description": "The type of entity being reported (Chat or Review)." + }, + "item_id": { + "type": "string", + "example": "1", + "description": "The ID of the conversation or review being reported." + }, + "user_id": { + "type": "string", + "example": "5", + "description": "Optional ID of the user being reported." + }, + "reason": { + "type": "string", + "example": "Others", + "description": "The reason for submitting this report." + }, + "others": { + "type": "string", + "example": "Spam and malicious links", + "description": "Custom reason string. Used only if reason is 'Others'." + }, + "description": { + "type": "string", + "example": "The other party sent highly inappropriate and insulting messages.", + "description": "Optional detailed explanation/comments." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Report submitted successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Report submitted successfully. Our safety team will review it shortly." + }, + "report_id": { + "type": "string", + "example": "REP-ABCD1234" + } + } + } + } + } + }, + "404": { + "description": "Conversation or Review not found, or user is not authorized to report it." + }, + "422": { + "description": "Validation error." + } + } + } + }, + "/workers/report-reasons": { + "get": { + "tags": [ + "Worker/Profile" + ], + "summary": "Get Safety Report Reasons (Worker)", + "description": "Returns a list of active safety/moderation report reasons. Can filter reasons by target type (Chat or Review).", + "parameters": [ + { + "name": "type", + "in": "query", + "required": false, + "description": "Optional type to filter reasons (Chat or Review). If not provided, returns all active reasons.", + "schema": { + "type": "string", + "enum": [ + "Chat", + "Review" + ] + } + } + ], + "responses": { + "200": { + "description": "List of report reasons retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "reasons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "reason": { + "type": "string", + "example": "Abusive language / Profanity" + }, + "type": { + "type": "string", + "example": "Both" + } + } + } + } + } + } + } + } + } + } + } + }, + "/workers/tickets": { + "get": { + "tags": [ + "Worker/Support" + ], + "summary": "List Support Tickets (Worker)", + "description": "Retrieves a paginated list of all support tickets created by the authenticated worker.", + "responses": { + "200": { + "description": "Tickets retrieved successfully." + } + } + }, + "post": { + "tags": [ + "Worker/Support" + ], + "summary": "Create Support Ticket (Worker)", + "description": "Creates a support ticket for a worker with a subject, description, and optional priority level.", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "subject", + "description" + ], + "properties": { + "subject": { + "type": "string", + "example": "App login crashes on launch" + }, + "description": { + "type": "string", + "example": "When opening the app on my phone, it crashes immediately on the splash screen." + }, + "priority": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "example": "medium" + }, + "reason_id": { + "type": "integer", + "example": 1, + "description": "Optional report/support reason ID." + }, + "voice_note": { + "type": "string", + "format": "binary", + "description": "Optional audio/voice note file attachment." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Ticket created successfully." + } + } + } + }, + "/workers/tickets/{id}": { + "get": { + "tags": [ + "Worker/Support" + ], + "summary": "Get Support Ticket Details & Replies (Worker)", + "description": "Retrieves the details of a support ticket and its chronological reply history.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Ticket details and replies retrieved successfully." + } + } + } + }, + "/workers/tickets/{id}/reply": { + "post": { + "tags": [ + "Worker/Support" + ], + "summary": "Reply to Support Ticket (Worker)", + "description": "Submits a text reply or message to an active, open, or resolved support ticket.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "I tried clearing cache and it still crashes." + }, + "voice_note": { + "type": "string", + "format": "binary", + "description": "Optional audio/voice note file attachment." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Reply posted successfully." + } + } + } + }, + "/employers/register": { + "post": { + "tags": [ + "Employer/Auth" + ], + "summary": "Register Sponsor (Alias)", + "description": "Legacy route matching the simplified sponsor registration (Name, Email, Phone) (Step 1).", + "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" + }, + "emirates_id": { + "type": "object", + "description": "Optional JSON object containing extracted Emirates ID details.", + "properties": { + "emirates_id_number": { + "type": "string", + "example": "784-1988-5310327-2" + }, + "name": { + "type": "string", + "example": "Ahmad Bin Ahmed" + }, + "date_of_birth": { + "type": "string", + "example": "1990-01-01" + }, + "nationality": { + "type": "string", + "example": "Emirati" + }, + "issue_date": { + "type": "string", + "example": "2023-04-11" + }, + "expiry_date": { + "type": "string", + "example": "2028-04-11" + }, + "employer": { + "type": "string", + "example": "Federal Authority" + }, + "issue_place": { + "type": "string", + "example": "Abu Dhabi" + }, + "occupation": { + "type": "string", + "example": "Manager" + } + } + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example", + "description": "Firebase Cloud Messaging token for push notifications." + }, + "address": { + "type": "string", + "example": "Villa 45, Street 12", + "description": "Physical address/location details." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Sponsor registered successfully." + } + } + } + }, + "/employers/verify": { + "post": { + "tags": [ + "Employer/Auth" + ], + "summary": "Verify Sponsor Email OTP", + "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" + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example", + "description": "Firebase Cloud Messaging token for push notifications." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Email verified successfully." + } + } + } + }, + "/employers/payment": { + "post": { + "tags": [ + "Employer/Auth" + ], + "summary": "Sponsor Subscription Payment", + "description": "Submits a successful plan selection and PayTabs payment confirmation (Step 4).", + "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": [ + "Employer/Auth" + ], + "summary": "Create Sponsor Account Password", + "description": "Configures and finalizes the portal login password to complete registration (Step 5). 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" + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example", + "description": "Firebase Cloud Messaging token for push notifications." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Password set and registration finalized successfully." + } + } + } + }, + "/employers/plans": { + "get": { + "tags": [ + "Employer/Auth" + ], + "summary": "Get Available Subscription Plans", + "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 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, + "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" + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example", + "description": "Firebase Cloud Messaging token for push notifications." + } + } + } + } + } + }, + "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": "employer", + "enum": [ + "employer", + "sponsor" + ] + }, + "data": { + "type": "object", + "properties": { + "token": { + "type": "string", + "example": "api_token_here" + } + } + } + } + } + } + } + } + } + } + }, + "/employers/conversations": { + "get": { + "tags": [ + "Employer/Conversations" + ], + "summary": "Get Conversations List (Employer)", + "description": "Retrieves all active candidate conversations for the authenticated employer.", + "responses": { + "200": { + "description": "Conversations list retrieved successfully." + } + } + } + }, + "/employers/conversations/{id}/messages": { + "get": { + "tags": [ + "Employer/Conversations" + ], + "summary": "Get Conversation Messages (Employer)", + "description": "Retrieves message history for a conversation and marks incoming worker messages as read.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Messages retrieved successfully." + } + } + }, + "post": { + "tags": [ + "Employer/Conversations" + ], + "summary": "Send Reply Message (Employer)", + "description": "Sends a reply message from the employer to the worker.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "example": "When can you start?", + "description": "Optional text message content (required if no file is uploaded)." + }, + "file": { + "type": "string", + "format": "binary", + "description": "Optional file or voice note attachment (Max 10MB). Supports images, documents, and common audio formats." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Message sent successfully." + } + } + } + }, + "/employers/conversations/start": { + "post": { + "tags": [ + "Employer/Conversations" + ], + "summary": "Start Conversation with Worker", + "description": "Starts or retrieves a conversation with the specified worker ID.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "worker_id" + ], + "properties": { + "worker_id": { + "type": "integer", + "example": 1 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Conversation started successfully." + } + } + } + }, + "/workers/announcements": { + "get": { + "tags": [ + "Worker/CharityEvents" + ], + "summary": "Get Charity Events (Worker)", + "description": "Allows workers to retrieve the list of active charity events, food drives, and medical support programs scheduled in Dubai.", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "description": "Page number.", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "description": "Number of items per page.", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "List of charity events retrieved successfully." + } + } + } + }, + "/workers/announcements/new": { + "get": { + "tags": [ + "Worker/CharityEvents" + ], + "summary": "Get New/Unviewed Announcements/Charity Events", + "description": "Retrieves up to 3 active approved charity events/announcements that the worker has not yet viewed, and automatically marks them as viewed.", + "responses": { + "200": { + "description": "Successfully retrieved new announcements.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 4 + }, + "title": { + "type": "string", + "example": "Free Dental Checkup Camp" + }, + "body": { + "type": "string", + "example": "Emirates Charity is providing free screening." + }, + "type": { + "type": "string", + "example": "charity" + }, + "employer_name": { + "type": "string", + "example": "Emirates Charity" + }, + "company_name": { + "type": "string", + "example": "Emirates Charity Foundation" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2026-06-01T10:00:00.000000Z" + }, + "time_ago": { + "type": "string", + "example": "5 hours ago" + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Unauthenticated." + } + } + } + }, + "/workers/announcements/{id}/view": { + "post": { + "tags": [ + "Worker/CharityEvents" + ], + "summary": "Mark Specific Announcement as Viewed", + "description": "Explicitly marks a specific announcement/charity event as viewed by the authenticated worker.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "Announcement ID", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Announcement marked as viewed successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Announcement marked as viewed." + } + } + } + } + } + }, + "401": { + "description": "Unauthenticated." + }, + "404": { + "description": "Announcement not found." + } + } + } + }, + "/employers/announcements": { + "get": { + "tags": [ + "Employer/CharityEvents" + ], + "summary": "Get Posted Charity Events (Employer)", + "description": "Allows employers/sponsors to retrieve the list of charity events they have published.", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "description": "Page number.", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "description": "Number of items per page.", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "List of posted charity events retrieved successfully." + } + } + }, + "post": { + "tags": [ + "Employer/CharityEvents" + ], + "summary": "Publish Charity Event (Employer)", + "description": "Allows employers/sponsors to publish a new community charity event or drive to all workers in Dubai, triggering instant push notifications and morning-of reminders.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "title", + "content", + "provided_items", + "event_date", + "event_time", + "location_details", + "location_pin" + ], + "properties": { + "title": { + "type": "string", + "example": "Free Dental Checkup Camp" + }, + "content": { + "type": "string", + "example": "Emirates Charity is providing free professional dental screening, cleanings, and educational packages for domestic helpers." + }, + "provided_items": { + "type": "string", + "example": "Free Dental screening, cleanings, and wellness kits" + }, + "event_date": { + "type": "string", + "format": "date", + "example": "2026-06-15" + }, + "event_time": { + "type": "string", + "example": "9:00 AM - 4:00 PM" + }, + "location_details": { + "type": "string", + "example": "Al Quoz Community Hall, Dubai" + }, + "location_pin": { + "type": "string", + "format": "uri", + "example": "https://maps.app.goo.gl/xyz" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Charity event created and notifications scheduled successfully." + } + } + } + }, + "/employers/announcements/{id}": { + "delete": { + "tags": [ + "Employer/CharityEvents" + ], + "summary": "Delete Charity Event (Employer)", + "description": "Allows employers/sponsors to delete a previously published charity event.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Charity event deleted successfully." + } + } + } + }, + "/employers/dashboard": { + "get": { + "tags": [ + "Employer/Profile" + ], + "summary": "Get Employer Dashboard stats & recent events", + "description": "Retrieves stats including contacted workers count, total hired workers, saved candidates, current plan details, and recent announcements or events.", + "responses": { + "200": { + "description": "Employer dashboard stats retrieved successfully." + } + } + } + }, + "/employers/profile": { + "get": { + "tags": [ + "Employer/Profile" + ], + "summary": "Get Profile details (Employer)", + "description": "Retrieves the authenticated employer's profile details including their selected company name, phone, language preference, and notification settings.", + "responses": { + "200": { + "description": "Employer profile details retrieved successfully." + } + } + } + }, + "/employers/profile/update": { + "post": { + "tags": [ + "Employer/Profile" + ], + "summary": "Update Profile details (Employer)", + "description": "Allows employers to update their profile details (company name, phone number, name, email, language preference, and notification settings) and change their account password.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "email", + "phone", + "company_name", + "language", + "notifications" + ], + "properties": { + "name": { + "type": "string", + "example": "Ahmad" + }, + "email": { + "type": "string", + "example": "ahmad@example.com" + }, + "phone": { + "type": "string", + "example": "+971509990001" + }, + "company_name": { + "type": "string", + "example": "Ahmad Tech Ltd" + }, + "language": { + "type": "string", + "enum": [ + "English", + "Arabic", + "english", + "arabic" + ], + "example": "English" + }, + "notifications": { + "type": "boolean", + "example": true + }, + "current_password": { + "type": "string", + "example": "Password@123" + }, + "new_password": { + "type": "string", + "example": "NewSecurePassword@123" + }, + "new_password_confirmation": { + "type": "string", + "example": "NewSecurePassword@123" + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example", + "description": "Firebase Cloud Messaging token for push notifications." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Profile updated successfully." + } + } + } + }, + "/employers/workers": { + "get": { + "tags": [ + "Employer/Pipeline" + ], + "summary": "Get Available Workers", + "description": "Returns a list of verified workers that are currently available to be hired.", + "parameters": [ + { + "name": "search", + "in": "query", + "required": false, + "description": "Search term matching helper name, nationality, or religion.", + "schema": { + "type": "string" + } + }, + { + "name": "skills", + "in": "query", + "required": false, + "description": "Filter by skills (comma-separated list).", + "schema": { + "type": "string" + } + }, + { + "name": "language", + "in": "query", + "required": false, + "description": "Filter by language.", + "schema": { + "type": "string" + } + }, + { + "name": "nationality", + "in": "query", + "required": false, + "description": "Filter by nationality name (comma-separated list for multiple choice, e.g. Indian,Kenyan,Ethiopian).", + "schema": { + "type": "string" + } + }, + { + "name": "gender", + "in": "query", + "required": false, + "description": "Filter by gender (e.g. male, female).", + "schema": { + "type": "string" + } + }, + { + "name": "preferred_location", + "in": "query", + "required": false, + "description": "Filter by preferred location (comma-separated list, e.g. Dubai,Abu Dhabi).", + "schema": { + "type": "string" + } + }, + { + "name": "area", + "in": "query", + "required": false, + "description": "Filter by preferred area (comma-separated list, e.g. Marina,Al Barsha).", + "schema": { + "type": "string" + } + }, + { + "name": "job_type", + "in": "query", + "required": false, + "description": "Filter by job type (comma-separated list, e.g. full-time,part-time).", + "schema": { + "type": "string" + } + }, + { + "name": "accommodation_type", + "in": "query", + "required": false, + "description": "Filter by accommodation type (comma-separated list, e.g. live_in,live_out).", + "schema": { + "type": "string" + } + }, + { + "name": "in_country", + "in": "query", + "required": false, + "description": "Filter by in country status (true/false or yes/no or in/out).", + "schema": { + "type": "string" + } + }, + { + "name": "out_country", + "in": "query", + "required": false, + "description": "Filter by out country status (true/false or yes/no or out/in).", + "schema": { + "type": "string" + } + }, + { + "name": "visa_status", + "in": "query", + "required": false, + "description": "Filter by next visa status (comma-separated list, e.g. Residence Visa,Tourist Visa). Only applicable if in country.", + "schema": { + "type": "string" + } + }, + { + "name": "availability", + "in": "query", + "required": false, + "description": "Filter by availability status.", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "description": "Page number.", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "description": "Number of items per page.", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Available workers list retrieved successfully." + } + } + } + }, + "/employers/workers/{id}": { + "get": { + "tags": [ + "Employer/Pipeline" + ], + "summary": "Get Worker Profile Detail", + "description": "Retrieves the complete profile details for a worker, mirroring the web detailed view, along with active conversation details if already started with this employer.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "Worker ID reference.", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Worker profile details retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "worker": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "Jane Doe" + }, + "nationality": { + "type": "string", + "example": "Filipino" + }, + "photo": { + "type": "string", + "example": "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200" + }, + "emirates_id_status": { + "type": "string", + "example": "Passport Verified", + "description": "Deprecated - Use passport_status instead" + }, + "passport_status": { + "type": "string", + "example": "Passport Verified" + }, + "category": { + "type": "string", + "example": "Domestic Worker" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "childcare", + "cooking" + ] + }, + "availability_status": { + "type": "string", + "example": "Active" + }, + "visa_status": { + "type": "string", + "example": "Residence Visa" + }, + "experience": { + "type": "string", + "example": "5 Years" + }, + "experience_years": { + "type": "integer", + "example": 5 + }, + "salary": { + "type": "integer", + "example": 2500 + }, + "religion": { + "type": "string", + "example": "Christian" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "English", + "Tagalog" + ] + }, + "age": { + "type": "integer", + "example": 28 + }, + "verified": { + "type": "boolean", + "example": true + }, + "preferred_job_type": { + "type": "string", + "example": "live-in" + }, + "bio": { + "type": "string", + "example": "Experienced and caring domestic worker specialing in childcare and housekeeping..." + }, + "rating": { + "type": "number", + "example": 4.3 + }, + "reviews_count": { + "type": "integer", + "example": 6 + }, + "reviews": { + "type": "array", + "items": { + "type": "object" + } + }, + "similar_workers": { + "type": "array", + "items": { + "type": "object" + } + }, + "conversation_id": { + "type": "integer", + "nullable": true, + "example": 4 + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Worker profile not found." + } + } + } + }, + "/employers/candidates": { + "get": { + "tags": [ + "Employer/Pipeline" + ], + "summary": "Get Candidate Applications and Offers", + "description": "Returns all job applications and direct hiring offers connected to the authenticated sponsor account that have achieved 'Hired' status.", + "parameters": [ + { + "name": "search", + "in": "query", + "required": false, + "description": "Search term matching helper name, nationality, or religion.", + "schema": { + "type": "string" + } + }, + { + "name": "skills", + "in": "query", + "required": false, + "description": "Filter candidates by skills (comma-separated list of skill names, e.g. cooking,driving).", + "schema": { + "type": "string" + } + }, + { + "name": "languages", + "in": "query", + "required": false, + "description": "Filter candidates by languages spoken (comma-separated list of language names, e.g. English,Arabic).", + "schema": { + "type": "string" + } + }, + { + "name": "nationality", + "in": "query", + "required": false, + "description": "Filter candidates by nationality name (comma-separated list for multiple choice, e.g. Indian,Kenyan,Ethiopian).", + "schema": { + "type": "string" + } + }, + { + "name": "gender", + "in": "query", + "required": false, + "description": "Filter candidates by gender (e.g. male, female).", + "schema": { + "type": "string" + } + }, + { + "name": "preferred_location", + "in": "query", + "required": false, + "description": "Filter candidates by preferred location (comma-separated list, e.g. Dubai,Abu Dhabi).", + "schema": { + "type": "string" + } + }, + { + "name": "job_type", + "in": "query", + "required": false, + "description": "Filter candidates by job type (comma-separated list, e.g. full-time,part-time).", + "schema": { + "type": "string" + } + }, + { + "name": "accommodation_type", + "in": "query", + "required": false, + "description": "Filter candidates by accommodation type (comma-separated list, e.g. live_in,live_out).", + "schema": { + "type": "string" + } + }, + { + "name": "in_country", + "in": "query", + "required": false, + "description": "Filter candidates by in country status (true/false or yes/no or in/out).", + "schema": { + "type": "string" + } + }, + { + "name": "out_country", + "in": "query", + "required": false, + "description": "Filter candidates by out country status (true/false or yes/no or out/in).", + "schema": { + "type": "string" + } + }, + { + "name": "visa_status", + "in": "query", + "required": false, + "description": "Filter candidates by next visa status (comma-separated list, e.g. Residence Visa,Tourist Visa). Only applicable if in country.", + "schema": { + "type": "string" + } + }, + { + "name": "availability", + "in": "query", + "required": false, + "description": "Filter candidates by availability status.", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "description": "Page number.", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "description": "Number of items per page.", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Employer candidate pipeline retrieved successfully." + } + } + } + }, + "/employers/candidates/{id}/hire": { + "post": { + "tags": [ + "Employer/Pipeline" + ], + "summary": "Mark Candidate as Hired", + "description": "Transitions the target candidate application, job offer, or helper status to 'Hired'.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The target identifier (Application ID, Offer ID, or Worker ID).", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Candidate successfully status updated to Hired." + } + } + } + }, + "/employers/shortlist": { + "get": { + "tags": [ + "Employer/Pipeline" + ], + "summary": "Get Shortlisted Workers", + "description": "Returns a list of all workers shortlisted by the authenticated sponsor.", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "description": "Page number.", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "description": "Number of items per page.", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Shortlisted workers retrieved successfully." + } + } + }, + "post": { + "tags": [ + "Employer/Pipeline" + ], + "summary": "Toggle Worker Shortlist Status", + "description": "Toggles (adds or removes) a worker to/from the authenticated sponsor's shortlist.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "worker_id" + ], + "properties": { + "worker_id": { + "type": "integer", + "example": 1, + "description": "Worker ID to shortlist/unshortlist." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Shortlist status toggled successfully." + } + } + } + }, + "/employers/payments": { + "get": { + "tags": [ + "Employer/Billing" + ], + "summary": "Get Payment History", + "description": "Retrieves the complete transaction history for the authenticated sponsor/employer, including subscription passes and document verification receipts.", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "description": "Page number.", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "description": "Number of items per page.", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Employer billing history retrieved successfully." + } + } + } + }, + "/workers/dashboard/views": { + "get": { + "tags": [ + "Worker/Dashboard" + ], + "summary": "Get Worker Profile Views Statistics", + "description": "Allows authenticated workers to see the total count and detailed list of employers/sponsors who viewed their full profile.", + "responses": { + "200": { + "description": "Profile views statistics retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "views_count": { + "type": "integer", + "example": 5 + }, + "views_list": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "employer_id": { + "type": "integer", + "example": 2 + }, + "employer_name": { + "type": "string", + "example": "Ahmad" + }, + "company_name": { + "type": "string", + "example": "Ahmad Tech Ltd" + }, + "nationality": { + "type": "string", + "example": "UAE" + }, + "city": { + "type": "string", + "example": "Dubai" + }, + "viewed_at": { + "type": "string", + "format": "date-time", + "example": "2026-06-01T15:10:24.000000Z" + }, + "viewed_at_formatted": { + "type": "string", + "example": "2 minutes ago" + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/workers/dashboard": { + "get": { + "tags": [ + "Worker/Dashboard" + ], + "summary": "Get Worker Consolidated Dashboard Data", + "description": "Allows authenticated workers to retrieve their dashboard metrics: active status, profile views count, currently working sponsor/employer details, and latest charity events/announcements.", + "responses": { + "200": { + "description": "Worker dashboard details retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "active_status": { + "type": "string", + "example": "active" + }, + "count_sponsors_viewed": { + "type": "integer", + "example": 5 + }, + "employer_contacted": { + "type": "integer", + "example": 2, + "description": "Unique count of employers who contacted this worker." + }, + "profile_viewed": { + "type": "integer", + "example": 3, + "description": "Unique count of employers who viewed this worker's profile." + }, + "currently_working_sponsor": { + "type": "object", + "nullable": true, + "properties": { + "id": { + "type": "integer", + "example": 2 + }, + "name": { + "type": "string", + "example": "Ahmad" + }, + "company_name": { + "type": "string", + "example": "Ahmad Tech Ltd" + }, + "nationality": { + "type": "string", + "example": "UAE" + }, + "city": { + "type": "string", + "example": "Dubai" + }, + "email": { + "type": "string", + "example": "ahmad@example.com" + }, + "phone": { + "type": "string", + "example": "+971509990001" + }, + "hired_at": { + "type": "string", + "format": "date-time", + "example": "2026-06-01T15:10:24.000000Z" + }, + "hired_at_formatted": { + "type": "string", + "example": "Jun 01, 2026" + } + } + }, + "latest_charity_events_list": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "title": { + "type": "string", + "example": "Free Dental Checkup Camp" + }, + "body": { + "type": "string", + "example": "Emirates Charity is providing free screening." + }, + "type": { + "type": "string", + "example": "charity" + }, + "employer_name": { + "type": "string", + "example": "Emirates Charity" + }, + "company_name": { + "type": "string", + "example": "Emirates Charity Foundation" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2026-06-01T10:00:00.000000Z" + }, + "time_ago": { + "type": "string", + "example": "5 hours ago" + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/employers/reviews": { + "get": { + "tags": [ + "Employer/Reviews" + ], + "summary": "Get Reviews Written by Employer", + "description": "Retrieves a paginated list of all reviews written by the authenticated employer.", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "description": "Page number.", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "description": "Number of items per page.", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "Reviews retrieved successfully." + } + } + }, + "post": { + "tags": [ + "Employer/Reviews" + ], + "summary": "Add or Update Worker Review", + "description": "Allows employers to submit a review score and description comment for a worker. If the employer has already left a review, this will update it.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "worker_id", + "rating" + ], + "properties": { + "worker_id": { + "type": "integer", + "example": 136, + "description": "Worker ID being reviewed." + }, + "rating": { + "type": "integer", + "minimum": 1, + "maximum": 5, + "example": 5, + "description": "Review score from 1 to 5." + }, + "comment": { + "type": "string", + "example": "Highly professional, punctual and did a great job!", + "description": "Optional detailed comment." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Review updated successfully." + }, + "201": { + "description": "Review added successfully." + }, + "422": { + "description": "Validation error." + } + } + } + }, + "/employers/reviews/{id}": { + "put": { + "tags": [ + "Employer/Reviews" + ], + "summary": "Edit Worker Review", + "description": "Allows employers to edit their previously created review score and comment by review ID.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "Review ID reference.", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "rating" + ], + "properties": { + "rating": { + "type": "integer", + "minimum": 1, + "maximum": 5, + "example": 4, + "description": "Updated rating score from 1 to 5." + }, + "comment": { + "type": "string", + "example": "Updated feedback comment.", + "description": "Updated review comment description." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Review edited successfully." + }, + "404": { + "description": "Review not found or unauthorized." + }, + "422": { + "description": "Validation error." + } + } + } + }, + "/employers/report": { + "post": { + "tags": [ + "Employer/Reviews" + ], + "summary": "Submit a Safety/Moderation Report (Employer)", + "description": "Submits a report on a chat conversation or a review. The report is logged in the safety queue and reviewed by administrators.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "type", + "item_id", + "reason" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Chat", + "Review" + ], + "example": "Chat", + "description": "The type of entity being reported (Chat or Review)." + }, + "item_id": { + "type": "string", + "example": "1", + "description": "The ID of the conversation or review being reported." + }, + "user_id": { + "type": "string", + "example": "5", + "description": "Optional ID of the user being reported." + }, + "reason": { + "type": "string", + "example": "Others", + "description": "The reason for submitting this report." + }, + "others": { + "type": "string", + "example": "Spam and malicious links", + "description": "Custom reason string. Used only if reason is 'Others'." + }, + "description": { + "type": "string", + "example": "The worker requested payments off-platform and sent abusive messages.", + "description": "Optional detailed explanation/comments." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Report submitted successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Report submitted successfully. Our safety team will review it shortly." + }, + "report_id": { + "type": "string", + "example": "REP-ABCD1234" + } + } + } + } + } + }, + "404": { + "description": "Conversation or Review not found, or user is not authorized to report it." + }, + "422": { + "description": "Validation error." + } + } + } + }, + "/employers/report-reasons": { + "get": { + "tags": [ + "Employer/Reviews" + ], + "summary": "Get Safety Report Reasons (Employer)", + "description": "Returns a list of active safety/moderation report reasons. Can filter reasons by target type (Chat or Review).", + "parameters": [ + { + "name": "type", + "in": "query", + "required": false, + "description": "Optional type to filter reasons (Chat or Review). If not provided, returns all active reasons.", + "schema": { + "type": "string", + "enum": [ + "Chat", + "Review" + ] + } + } + ], + "responses": { + "200": { + "description": "List of report reasons retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "reasons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "reason": { + "type": "string", + "example": "Abusive language / Profanity" + }, + "type": { + "type": "string", + "example": "Both" + } + } + } + } + } + } + } + } + } + } + } + }, + "/employers/tickets": { + "get": { + "tags": [ + "Employer/Support" + ], + "summary": "List Support Tickets (Employer)", + "description": "Retrieves a paginated list of all support tickets created by the authenticated employer.", + "responses": { + "200": { + "description": "Tickets retrieved successfully." + } + } + }, + "post": { + "tags": [ + "Employer/Support" + ], + "summary": "Create Support Ticket (Employer)", + "description": "Creates a support ticket for an employer with a subject, description, and optional priority level.", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "subject", + "description" + ], + "properties": { + "subject": { + "type": "string", + "example": "Payment card rejected" + }, + "description": { + "type": "string", + "example": "My Visa card was declined when trying to buy the Premium Employer Pass." + }, + "priority": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "example": "medium" + }, + "reason_id": { + "type": "integer", + "example": 1, + "description": "Optional report/support reason ID." + }, + "voice_note": { + "type": "string", + "format": "binary", + "description": "Optional audio/voice note file attachment." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Ticket created successfully." + } + } + } + }, + "/employers/tickets/{id}": { + "get": { + "tags": [ + "Employer/Support" + ], + "summary": "Get Support Ticket Details & Replies (Employer)", + "description": "Retrieves the details of a support ticket and its chronological reply history.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Ticket details and replies retrieved successfully." + } + } + } + }, + "/employers/tickets/{id}/reply": { + "post": { + "tags": [ + "Employer/Support" + ], + "summary": "Reply to Support Ticket (Employer)", + "description": "Submits a text reply or message to an active, open, or resolved support ticket.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "I tried a different card and it still fails." + }, + "voice_note": { + "type": "string", + "format": "binary", + "description": "Optional audio/voice note file attachment." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Reply posted successfully." + } + } + } + }, + "/workers/forgot-password": { + "post": { + "tags": [ + "Workers - Auth" + ], + "summary": "Forgot Password (Send OTP)", + "description": "Sends a 6-digit OTP to verify the worker's identity using their registered mobile number. Workers do not use email — mobile/phone is the primary identifier. OTP is valid for 10 minutes.", + "operationId": "workerForgotPassword", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "phone" + ], + "properties": { + "phone": { + "type": "string", + "example": "+971501234567", + "description": "Worker's registered mobile number" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OTP sent successfully (response is identical whether account exists or not, to prevent enumeration)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "If an account with that mobile number exists, a reset OTP has been sent." + } + } + } + } + } + }, + "422": { + "description": "Validation error — phone is required" + } + } + } + }, + "/workers/reset-password": { + "post": { + "tags": [ + "Workers - Auth" + ], + "summary": "Reset Password (Verify OTP)", + "description": "Verifies the OTP sent to the worker's mobile number and sets a new password. The phone number must match the one used in the forgot-password step.", + "operationId": "workerResetPassword", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "phone", + "otp", + "password", + "password_confirmation" + ], + "properties": { + "phone": { + "type": "string", + "example": "+971501234567", + "description": "Worker's registered mobile number" + }, + "otp": { + "type": "string", + "example": "482910", + "description": "6-digit OTP received" + }, + "password": { + "type": "string", + "example": "newpassword123", + "description": "New password (min 8 chars)" + }, + "password_confirmation": { + "type": "string", + "example": "newpassword123", + "description": "Must match password" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Password reset successfully" + }, + "422": { + "description": "Invalid OTP, expired OTP, or validation error" + }, + "404": { + "description": "Account not found" + } + } + } + }, + "/employers/forgot-password": { + "post": { + "tags": [ + "Employers - Auth" + ], + "summary": "Forgot Password (Send OTP)", + "description": "Sends a 6-digit OTP to the employer's registered email address. OTP expires in 10 minutes.", + "operationId": "employerForgotPassword", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "employer@example.com" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OTP sent (response identical whether account exists or not)" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/employers/reset-password": { + "post": { + "tags": [ + "Employers - Auth" + ], + "summary": "Reset Password (Verify OTP)", + "description": "Verifies the OTP sent to the employer's email and sets a new password.", + "operationId": "employerResetPassword", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email", + "otp", + "password", + "password_confirmation" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "employer@example.com" + }, + "otp": { + "type": "string", + "example": "482910" + }, + "password": { + "type": "string", + "example": "newpassword123" + }, + "password_confirmation": { + "type": "string", + "example": "newpassword123" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Password reset successfully" + }, + "422": { + "description": "Invalid or expired OTP" + } + } + } + }, + "/sponsors/forgot-password": { + "post": { + "tags": [ + "Sponsors - Auth" + ], + "summary": "Forgot Password (Send OTP)", + "description": "Sends a 6-digit OTP to the sponsor's registered email. Accepts email or mobile number as identifier. OTP expires in 10 minutes.", + "operationId": "sponsorForgotPassword", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "sponsor@example.com", + "description": "Email address (either email or mobile required)" + }, + "mobile": { + "type": "string", + "example": "+971501234567", + "description": "Mobile number (either email or mobile required)" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OTP sent (response identical whether account exists or not)" + }, + "422": { + "description": "Validation error — email or mobile required" + } + } + } + }, + "/sponsors/reset-password": { + "post": { + "tags": [ + "Sponsors - Auth" + ], + "summary": "Reset Password (Verify OTP)", + "description": "Verifies the OTP sent to the sponsor's email and sets a new password. Use the email field to identify the account.", + "operationId": "sponsorResetPassword", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email", + "otp", + "password", + "password_confirmation" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "sponsor@example.com" + }, + "otp": { + "type": "string", + "example": "482910" + }, + "password": { + "type": "string", + "example": "newpassword123" + }, + "password_confirmation": { + "type": "string", + "example": "newpassword123" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Password reset successfully" + }, + "422": { + "description": "Invalid or expired OTP" + } + } + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, + "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" + }, + "emirates_id_file": { + "type": "string", + "example": "uploads/licenses/1234567890_emirates_id.pdf" + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example" + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "Worker": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 136 + }, + "name": { + "type": "string", + "example": "Amina Al-Masry" + }, + "email": { + "type": "string", + "example": "worker.971509998888@migrant.com" + }, + "phone": { + "type": "string", + "example": "+971509998888" + }, + "language": { + "type": "string", + "example": "HI" + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example" + }, + "nationality": { + "type": "string", + "example": "Egypt" + }, + "age": { + "type": "integer", + "example": 28 + }, + "salary": { + "type": "string", + "example": "1500.00" + }, + "availability": { + "type": "string", + "example": "Immediate" + }, + "experience": { + "type": "string", + "example": "4 Years" + }, + "religion": { + "type": "string", + "example": "Muslim" + }, + "bio": { + "type": "string", + "example": "Certified infant caregiver and housekeeper with excellent cooking skills." + }, + "verified": { + "type": "boolean", + "example": false + }, + "status": { + "type": "string", + "example": "active" + }, + "in_country": { + "type": "boolean", + "example": true + }, + "visa_status": { + "type": "string", + "example": "Tourist Visa" + }, + "passport_status": { + "type": "string", + "example": "Passport Verified" + }, + "emirates_id_status": { + "type": "string", + "example": "Passport Verified" + }, + "preferred_job_type": { + "type": "string", + "example": "full-time" + }, + "live_in_out": { + "type": "string", + "example": "Live-out" + }, + "gender": { + "type": "string", + "enum": [ + "male", + "female", + "other" + ], + "example": "male" + }, + "preferred_location": { + "type": "string", + "example": "Dubai Marina" + }, + "country": { + "type": "string", + "example": "UAE" + }, + "city": { + "type": "string", + "example": "Dubai" + }, + "area": { + "type": "string", + "example": "Marina" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2026-05-20T14:54:26.000000Z" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "example": "2026-05-20T14:54:26.000000Z" + }, + "category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 7 + }, + "name": { + "type": "string", + "example": "General Helper" + } + } + }, + "skills": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 6 + }, + "name": { + "type": "string", + "example": "Cleaning" + } + } + } + }, + "documents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkerDocument" + } + } + } + }, + "WorkerDocument": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 15 + }, + "worker_id": { + "type": "integer", + "example": 136 + }, + "type": { + "type": "string", + "example": "passport" + }, + "number": { + "type": "string", + "example": "P345892" + }, + "issue_date": { + "type": "string", + "format": "date", + "example": "2024-05-20" + }, + "expiry_date": { + "type": "string", + "format": "date", + "example": "2034-05-20" + }, + "ocr_accuracy": { + "type": "number", + "example": 98.5 + }, + "file_path": { + "type": "string", + "example": "uploads/documents/1716200000_passport_my_file.jpg" + } + } + }, + "JobOffer": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "employer_id": { + "type": "integer", + "example": 2 + }, + "worker_id": { + "type": "integer", + "example": 136 + }, + "work_date": { + "type": "string", + "format": "date", + "example": "2026-06-01" + }, + "location": { + "type": "string", + "example": "Dubai Marina, Silverene Towers" + }, + "salary": { + "type": "string", + "example": "2500.00" + }, + "notes": { + "type": "string", + "example": "Require housekeeping support starting from June 1st." + }, + "status": { + "type": "string", + "example": "pending" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2026-05-20T15:23:44.000000Z" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "example": "2026-05-20T15:23:44.000000Z" + } + } + } + } + } +} \ No newline at end of file diff --git a/public/uploads/licenses/1780656858_license_license.pdf b/public/uploads/licenses/1780656858_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780656874_license_license.pdf b/public/uploads/licenses/1780656874_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780656902_license_license.pdf b/public/uploads/licenses/1780656902_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780657376_license_license.pdf b/public/uploads/licenses/1780657376_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780658014_license_license.pdf b/public/uploads/licenses/1780658014_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780910940_license_license.pdf b/public/uploads/licenses/1780910940_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780911257_license_license.pdf b/public/uploads/licenses/1780911257_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780911277_license_license.pdf b/public/uploads/licenses/1780911277_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780911292_license_license.pdf b/public/uploads/licenses/1780911292_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780911308_license_license.pdf b/public/uploads/licenses/1780911308_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780911321_license_license.pdf b/public/uploads/licenses/1780911321_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780912272_license_license.pdf b/public/uploads/licenses/1780912272_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780912872_license_license.pdf b/public/uploads/licenses/1780912872_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780914822_license_license.pdf b/public/uploads/licenses/1780914822_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780988093_license_license.pdf b/public/uploads/licenses/1780988093_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780988183_license_license.pdf b/public/uploads/licenses/1780988183_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780988391_license_license.pdf b/public/uploads/licenses/1780988391_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780988597_license_license.pdf b/public/uploads/licenses/1780988597_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780988676_license_license.pdf b/public/uploads/licenses/1780988676_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780988798_license_license.pdf b/public/uploads/licenses/1780988798_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780988812_license_license.pdf b/public/uploads/licenses/1780988812_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780988844_license_license.pdf b/public/uploads/licenses/1780988844_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780989036_license_license.pdf b/public/uploads/licenses/1780989036_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780989071_license_license.pdf b/public/uploads/licenses/1780989071_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780989258_license_license.pdf b/public/uploads/licenses/1780989258_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780990035_license_license.pdf b/public/uploads/licenses/1780990035_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780990120_license_license.pdf b/public/uploads/licenses/1780990120_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780990245_license_license.pdf b/public/uploads/licenses/1780990245_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780990328_license_license.pdf b/public/uploads/licenses/1780990328_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780990399_license_license.pdf b/public/uploads/licenses/1780990399_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780990462_license_license.pdf b/public/uploads/licenses/1780990462_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780990748_license_license.pdf b/public/uploads/licenses/1780990748_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780991005_license_license.pdf b/public/uploads/licenses/1780991005_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780991060_license_license.pdf b/public/uploads/licenses/1780991060_license_license.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/public/uploads/licenses/1780997733_license_fDi5IHoHP14P_1024_500.png b/public/uploads/licenses/1780997733_license_fDi5IHoHP14P_1024_500.png deleted file mode 100644 index 24d963e79b9c3a1d57a8423feb6758ced9373707..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83228 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 - Worker Information - Placement Status - Verification Status - Exp & Languages - Location Details - Lifecycle + Worker + Languages + Nationality + Preferred Location + Accommodation + Salary + Status Actions {filteredWorkers.length > 0 ? ( - filteredWorkers.map((worker) => ( - - -
-
- {worker.name.charAt(0)} -
-
-
- {worker.name} - {worker.id === 103 && ( - - Suspicious - - )} + filteredWorkers.map((worker) => { + const languages = worker.languages || (worker.language ? worker.language.split(',').map(l => l.trim()) : []); + const isLiveIn = worker.live_in_out?.toLowerCase().includes('in') || worker.live_in_out?.toLowerCase().includes('stay'); + + return ( + + +
+
+ {worker.name.charAt(0)}
-
{worker.phone}
-
-
- - - - {worker.availability} - - - -
-
- -
- - {worker.verified ? 'OCR Verified' : 'Pending Verification'} - -
-
- -
-
- {worker.nationality} • {worker.experience} -
-
- Lang: {worker.language} -
-
-
- -
- {worker.country ? ( - <> -
{worker.city}, {worker.country}
-
{worker.area}
- - ) : ( - Not set - )} -
-
- - - {worker.status} - - - -
- - - - - - - Quick Actions - - handleToggleStatus(worker.id, worker.status === 'active' ? 'suspended' : 'active')} - className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-red-600 hover:bg-red-50" - > - {worker.status === 'active' ? ( - <> Suspend Account - ) : ( - <> Activate Account +
+
+ {worker.name} + {worker.id === 103 && ( + + Suspicious + )} - +
+
+ + {worker.gender || 'Female'} + {worker.age && • {worker.age} Yrs} +
+
+ + {worker.phone} +
+
+
+
+ +
+ {languages.length > 0 ? languages.map((lang, idx) => ( + + {lang} + + )) : ( + English + )} +
+
+ +
+ + {worker.nationality} +
+
+ +
+ {worker.preferred_location ? ( +
+ + {worker.preferred_location} +
+ ) : worker.city ? ( +
+ +
+
{worker.city}
+
{worker.area}
+
+
+ ) : ( + Not set + )} +
+
+ + + {isLiveIn ? 'Live-in' : 'Live-out'} + + + +
+ {worker.salary ? `${worker.salary.toLocaleString()} AED` : 'N/A'} + / month +
+
+ + + {worker.status} + + + +
+ + + + + + + Quick Actions - - -
-
- - )) + handleToggleStatus(worker.id, worker.status === 'active' ? 'suspended' : 'active')} + className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-red-600 hover:bg-red-50" + > + {worker.status === 'active' ? ( + <> Suspend Account + ) : ( + <> Activate Account + )} + + + + +
+ + + ); + }) ) : ( - +
No workers found matching your criteria.
diff --git a/tests/Feature/WorkerJourneyApiTest.php b/tests/Feature/WorkerJourneyApiTest.php index be4f752..7e8487b 100644 --- a/tests/Feature/WorkerJourneyApiTest.php +++ b/tests/Feature/WorkerJourneyApiTest.php @@ -216,7 +216,6 @@ public function test_s2_update_experience_and_preferences() ])->postJson('/api/workers/profile/update', [ 'experience' => '3 Years Cooking & Childcare', 'salary' => 1800, - 'bio' => 'Extremely experienced in Arabic and Western cooking.', ]); $response->assertStatus(200); @@ -633,9 +632,6 @@ public function test_update_profile_with_new_api_fields() 'gender' => 'female', 'live_in_out' => 'live_in', 'preferred_location' => 'Abu Dhabi', - 'country' => 'UAE', - 'city' => 'Abu Dhabi', - 'area' => 'Khalifa City', ]); $response->assertStatus(200); @@ -648,9 +644,6 @@ public function test_update_profile_with_new_api_fields() 'gender' => 'female', 'live_in_out' => 'live_in', 'preferred_location' => 'Abu Dhabi', - 'country' => 'UAE', - 'city' => 'Abu Dhabi', - 'area' => 'Khalifa City', ]); } @@ -1234,4 +1227,208 @@ public function test_register_validation_fails_if_passport_number_already_exists $response->json('errors')['passport.passport_number'][0] ); } + + /** + * Test profile update fails if updating a passport document with a different passport number. + */ + public function test_update_profile_fails_if_passport_number_does_not_match_existing() + { + $worker = Worker::create([ + 'name' => 'Test Worker', + 'email' => 'test@example.com', + 'phone' => '+971503333333', + 'language' => 'HI', + 'password' => bcrypt('password'), + 'nationality' => 'Indian', + 'age' => 25, + 'salary' => 1500, + 'availability' => 'Immediate', + 'experience' => 'None', + 'religion' => 'Not Specified', + 'bio' => 'New helper', + 'verified' => false, + 'status' => 'active', + 'api_token' => 'bearer-test-token-passport-mismatch', + ]); + + $worker->documents()->create([ + 'type' => 'passport', + 'number' => 'ABC123456', + 'issue_date' => '2020-01-01', + 'expiry_date' => '2030-01-01', + 'ocr_accuracy' => 95.0, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer bearer-test-token-passport-mismatch', + ])->postJson('/api/workers/profile/update', [ + 'passport' => [ + 'passport_number' => 'XYZ987654', // Different passport number + ] + ]); + + $response->assertStatus(422) + ->assertJsonStructure([ + 'success', + 'message', + 'errors' => [ + 'passport.passport_number' + ] + ]); + + $this->assertEquals( + 'The passport number cannot be changed. It must match the previously verified passport number.', + $response->json('errors')['passport.passport_number'][0] + ); + } + + /** + * Test profile update succeeds if passport number matches existing. + */ + public function test_update_profile_succeeds_if_passport_number_matches_existing() + { + $worker = Worker::create([ + 'name' => 'Test Worker', + 'email' => 'test2@example.com', + 'phone' => '+971503333334', + 'language' => 'HI', + 'password' => bcrypt('password'), + 'nationality' => 'Indian', + 'age' => 25, + 'salary' => 1500, + 'availability' => 'Immediate', + 'experience' => 'None', + 'religion' => 'Not Specified', + 'bio' => 'New helper', + 'verified' => false, + 'status' => 'active', + 'api_token' => 'bearer-test-token-passport-match', + ]); + + $worker->documents()->create([ + 'type' => 'passport', + 'number' => 'ABC123456', + 'issue_date' => '2020-01-01', + 'expiry_date' => '2030-01-01', + 'ocr_accuracy' => 95.0, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer bearer-test-token-passport-match', + ])->postJson('/api/workers/profile/update', [ + 'passport' => [ + 'passport_number' => 'ABC123456', // Same passport number + 'date_of_expiry' => '2032-01-01', // Updating expiry date + ] + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('worker_documents', [ + 'worker_id' => $worker->id, + 'type' => 'passport', + 'number' => 'ABC123456', + 'expiry_date' => '2032-01-01', + ]); + } + + /** + * Test unique mobile number validation on profile update. + */ + public function test_update_profile_fails_if_phone_already_exists() + { + // 1. Create original worker with phone +971504444444 + Worker::create([ + 'name' => 'Other Worker', + 'email' => 'other@example.com', + 'phone' => '+971504444444', + 'language' => 'HI', + 'password' => bcrypt('password'), + 'nationality' => 'Indian', + 'age' => 25, + 'salary' => 1500, + 'availability' => 'Immediate', + 'experience' => 'None', + 'religion' => 'Not Specified', + 'bio' => 'New helper', + 'verified' => false, + 'status' => 'active', + ]); + + // 2. Create current worker with phone +971505555555 + $worker = Worker::create([ + 'name' => 'Current Worker', + 'email' => 'current@example.com', + 'phone' => '+971505555555', + 'language' => 'HI', + 'password' => bcrypt('password'), + 'nationality' => 'Indian', + 'age' => 25, + 'salary' => 1500, + 'availability' => 'Immediate', + 'experience' => 'None', + 'religion' => 'Not Specified', + 'bio' => 'New helper', + 'verified' => false, + 'status' => 'active', + 'api_token' => 'bearer-test-token-phone-unique', + ]); + + // 3. Attempt to update current worker's phone to the other worker's phone + $response = $this->withHeaders([ + 'Authorization' => 'Bearer bearer-test-token-phone-unique', + ])->postJson('/api/workers/profile/update', [ + 'phone' => '+971504444444', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['phone']); + + $this->assertEquals( + 'This mobile number is already registered.', + $response->json('errors')['phone'][0] + ); + } + + /** + * Test deprecated fields are hidden from JSON response. + */ + public function test_profile_response_does_not_contain_deprecated_fields() + { + $worker = Worker::create([ + 'name' => 'Test Worker', + 'email' => 'test_dep@example.com', + 'phone' => '+971503333335', + 'language' => 'HI', + 'password' => bcrypt('password'), + 'nationality' => 'Indian', + 'age' => 25, + 'salary' => 1500, + 'availability' => 'Immediate', + 'experience' => 'None', + 'religion' => 'Christian', + 'bio' => 'Some Bio', + 'country' => 'UAE', + 'city' => 'Dubai', + 'area' => 'JLT', + 'verified' => false, + 'status' => 'active', + 'api_token' => 'bearer-test-token-deprecated-fields', + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer bearer-test-token-deprecated-fields', + ])->getJson('/api/workers/profile'); + + $response->assertStatus(200); + $json = $response->json('data.worker'); + + $this->assertArrayNotHasKey('email', $json); + $this->assertArrayNotHasKey('religion', $json); + $this->assertArrayNotHasKey('availability', $json); + $this->assertArrayNotHasKey('bio', $json); + $this->assertArrayNotHasKey('country', $json); + $this->assertArrayNotHasKey('city', $json); + $this->assertArrayNotHasKey('area', $json); + } } diff --git a/vite.config.js b/vite.config.js index a0a2baa..fa54835 100644 --- a/vite.config.js +++ b/vite.config.js @@ -23,7 +23,7 @@ export default defineConfig({ port: 5173, cors: true, hmr: { - host: '192.168.29.192', + host: '192.168.29.131', }, watch: { ignored: ['**/storage/framework/views/**'],