diff --git a/app/Http/Controllers/Api/EmployerAuthController.php b/app/Http/Controllers/Api/EmployerAuthController.php index d84ff46..b2716d0 100644 --- a/app/Http/Controllers/Api/EmployerAuthController.php +++ b/app/Http/Controllers/Api/EmployerAuthController.php @@ -198,6 +198,13 @@ public function register(Request $request) 'phone.unique' => 'This mobile number is already registered.', ]); + $validator->after(function ($validator) use ($request) { + $emiratesIdInput = $request->input('emirates_id'); + if (is_array($emiratesIdInput) && empty($emiratesIdInput['name'])) { + $validator->errors()->add('emirates_id.name', 'Failed to extract name from the Emirates ID. Please upload a clearer document.'); + } + }); + if ($validator->fails()) { return response()->json([ 'success' => false, @@ -232,6 +239,9 @@ public function register(Request $request) $emiratesIdNumber = $emiratesIdInput['emirates_id_number'] ?? $emiratesIdInput['id_number'] ?? $emiratesIdInput['id number'] ?? $emiratesIdInput['card_number'] ?? null; $emiratesIdExpiry = $emiratesIdInput['expiry_date'] ?? $emiratesIdInput['expiry date'] ?? $emiratesIdInput['emirates_id_expiry'] ?? null; $emiratesIdName = $emiratesIdInput['name'] ?? null; + if ($emiratesIdName) { + $request->merge(['name' => $emiratesIdName]); + } $emiratesIdDob = $emiratesIdInput['date_of_birth'] ?? $emiratesIdInput['date of birth'] ?? $emiratesIdInput['dob'] ?? null; $emiratesIdNationality = $emiratesIdInput['nationality'] ?? null; $emiratesIdIssueDate = $emiratesIdInput['issue_date'] ?? $emiratesIdInput['issue date'] ?? null; diff --git a/app/Http/Controllers/Api/EmployerProfileController.php b/app/Http/Controllers/Api/EmployerProfileController.php index e2c1559..abd25c1 100644 --- a/app/Http/Controllers/Api/EmployerProfileController.php +++ b/app/Http/Controllers/Api/EmployerProfileController.php @@ -133,6 +133,12 @@ public function updateProfile(Request $request) 'issuing_place' => 'nullable|string|max:255', ]); + $validator->after(function ($validator) use ($request) { + if ($request->has('full_name') && !$request->filled('full_name')) { + $validator->errors()->add('full_name', 'Failed to extract name from the Emirates ID. Please upload a clearer document.'); + } + }); + if ($validator->fails()) { return response()->json([ 'success' => false, @@ -142,6 +148,10 @@ public function updateProfile(Request $request) } try { + if ($request->filled('full_name')) { + $request->merge(['name' => $request->full_name]); + } + // Validate Emirates ID immutability — once set, it cannot be changed if ($request->filled('id_number')) { $existingProfile = $employer->employerProfile; diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php index 705859f..b37d03a 100644 --- a/app/Http/Controllers/Api/EmployerWorkerController.php +++ b/app/Http/Controllers/Api/EmployerWorkerController.php @@ -97,6 +97,7 @@ private function formatWorker(Worker $w) unset($ocrData['passport_number']); unset($ocrData['accompanied_by']); unset($ocrData['valid_until']); + unset($ocrData['sponsor']); } return [ 'id' => $doc->id, @@ -1008,6 +1009,7 @@ public function getWorkerDetail(Request $request, $id) unset($ocrData['passport_number']); unset($ocrData['accompanied_by']); unset($ocrData['valid_until']); + unset($ocrData['sponsor']); } return [ 'id' => $doc->id, diff --git a/app/Http/Controllers/Api/SponsorAuthController.php b/app/Http/Controllers/Api/SponsorAuthController.php index f16bff0..ab7ec5f 100644 --- a/app/Http/Controllers/Api/SponsorAuthController.php +++ b/app/Http/Controllers/Api/SponsorAuthController.php @@ -41,6 +41,13 @@ public function register(Request $request) 'email.unique' => 'This email address is already registered.', ]); + $validator->after(function ($validator) use ($request) { + $emiratesIdInput = $request->input('emirates_id'); + if (is_array($emiratesIdInput) && empty($emiratesIdInput['name'])) { + $validator->errors()->add('emirates_id.name', 'Failed to extract name from the Emirates ID. Please upload a clearer document.'); + } + }); + if ($validator->fails()) { return response()->json([ 'success' => false, @@ -59,8 +66,48 @@ public function register(Request $request) $licenseExpiry = $request->license_expiry; $licenseInput = $request->input('license'); + $licenseData = null; if (is_array($licenseInput)) { $licenseExpiry = $licenseExpiry ?? ($licenseInput['expiry_date'] ?? $licenseInput['license_expiry'] ?? null); + $licenseData = [ + 'document_type' => $licenseInput['document_type'] ?? null, + 'country' => $licenseInput['country'] ?? null, + 'authority' => $licenseInput['authority'] ?? null, + 'license_no' => $licenseInput['license_no'] ?? $licenseInput['license_number'] ?? null, + 'company_name' => $licenseInput['company_name'] ?? $licenseInput['organization_name'] ?? null, + 'business_name' => $licenseInput['business_name'] ?? null, + 'license_category' => $licenseInput['license_category'] ?? null, + 'legal_type' => $licenseInput['legal_type'] ?? null, + 'issue_date' => $licenseInput['issue_date'] ?? null, + 'expiry_date' => $licenseInput['expiry_date'] ?? ($licenseInput['license_expiry'] ?? null), + 'main_license_no' => $licenseInput['main_license_no'] ?? null, + 'register_no' => $licenseInput['register_no'] ?? null, + 'dcci_no' => $licenseInput['dcci_no'] ?? null, + 'members' => $licenseInput['members'] ?? null, + ]; + } else { + $licenseData = [ + 'document_type' => $request->document_type, + 'country' => $request->country, + 'authority' => $request->authority, + 'license_no' => $request->license_no, + 'company_name' => $request->company_name, + 'business_name' => $request->business_name, + 'license_category' => $request->license_category, + 'legal_type' => $request->legal_type, + 'issue_date' => $request->issue_date, + 'expiry_date' => $request->expiry_date, + 'main_license_no' => $request->main_license_no, + 'register_no' => $request->register_no, + 'dcci_no' => $request->dcci_no, + 'members' => $request->members, + ]; + } + + if (is_array($licenseData)) { + $licenseData = array_filter($licenseData, function ($val) { + return !is_null($val); + }); } $emiratesIdInput = $request->input('emirates_id'); @@ -78,6 +125,9 @@ public function register(Request $request) $emiratesId = $emiratesIdInput['emirates_id_number'] ?? $emiratesIdInput['id_number'] ?? $emiratesIdInput['id number'] ?? $emiratesIdInput['card_number'] ?? null; $emiratesIdExpiry = $emiratesIdInput['expiry_date'] ?? $emiratesIdInput['expiry date'] ?? $emiratesIdInput['emirates_id_expiry'] ?? null; $emiratesIdName = $emiratesIdInput['name'] ?? null; + if ($emiratesIdName) { + $request->merge(['full_name' => $emiratesIdName]); + } $emiratesIdDob = $emiratesIdInput['date_of_birth'] ?? $emiratesIdInput['date of birth'] ?? $emiratesIdInput['dob'] ?? null; $emiratesIdNationality = $emiratesIdInput['nationality'] ?? null; $emiratesIdIssueDate = $emiratesIdInput['issue_date'] ?? $emiratesIdInput['issue date'] ?? null; @@ -94,7 +144,7 @@ public function register(Request $request) $sponsor = DB::transaction(function () use ( $request, $email, $licensePath, $emiratesIdPath, $emiratesId, $apiToken, $licenseExpiry, $emiratesIdExpiry, $emiratesIdName, $emiratesIdDob, $emiratesIdIssueDate, $emiratesIdEmployer, - $emiratesIdIssuePlace, $emiratesIdOccupation + $emiratesIdIssuePlace, $emiratesIdOccupation, $licenseData ) { return Sponsor::create([ 'full_name' => $request->full_name, @@ -122,6 +172,7 @@ public function register(Request $request) 'emirates_id_employer' => $emiratesIdEmployer, 'emirates_id_issue_place' => $emiratesIdIssuePlace, 'emirates_id_occupation' => $emiratesIdOccupation, + 'license_data' => $licenseData, ]); }); diff --git a/app/Http/Controllers/Api/SponsorController.php b/app/Http/Controllers/Api/SponsorController.php index cea8fd7..69bd152 100644 --- a/app/Http/Controllers/Api/SponsorController.php +++ b/app/Http/Controllers/Api/SponsorController.php @@ -305,28 +305,73 @@ public function getProfile(Request $request) return response()->json([ 'success' => true, 'data' => [ - 'sponsor' => [ - 'id' => $sponsor->id, - 'full_name' => $sponsor->full_name, - 'organization_name' => $sponsor->organization_name, - 'mobile' => $sponsor->mobile, - 'email' => $sponsor->email, - 'nationality' => $sponsor->nationality, - 'city' => $sponsor->city, - 'address' => $sponsor->address, - 'country_code' => $sponsor->country_code, - 'is_verified' => $sponsor->is_verified, - 'status' => $sponsor->status, - 'license_file' => $sponsor->license_file ? asset($sponsor->license_file) : null, - 'emirates_id_file' => $sponsor->emirates_id_file ? asset($sponsor->emirates_id_file) : null, - 'license_expiry' => $sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : null, - 'validity' => $sponsor->license_expiry ? ($sponsor->license_expiry->isFuture() ? 'Valid' : 'Expired') : 'Pending Review', - 'joined_at' => $sponsor->created_at->toIso8601String(), - ] + 'sponsor' => $this->buildSponsorResponse($sponsor) ] ], 200); } + /** + * Build the standardized sponsor response array with nested license and emirates_id objects. + */ + private function buildSponsorResponse(Sponsor $sponsor): array + { + $licenseData = [ + 'license_number' => $sponsor->license_data['license_no'] ?? $sponsor->license_data['license_number'] ?? null, + 'organization_name' => $sponsor->organization_name, + 'expiry_date' => $sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : ($sponsor->license_data['expiry_date'] ?? null), + 'document_type' => $sponsor->license_data['document_type'] ?? null, + 'country' => $sponsor->license_data['country'] ?? null, + 'authority' => $sponsor->license_data['authority'] ?? null, + 'license_no' => $sponsor->license_data['license_no'] ?? null, + 'company_name' => $sponsor->license_data['company_name'] ?? null, + 'business_name' => $sponsor->license_data['business_name'] ?? null, + 'license_category' => $sponsor->license_data['license_category'] ?? null, + 'legal_type' => $sponsor->license_data['legal_type'] ?? null, + 'issue_date' => $sponsor->license_data['issue_date'] ?? null, + 'main_license_no' => $sponsor->license_data['main_license_no'] ?? null, + 'register_no' => $sponsor->license_data['register_no'] ?? null, + 'dcci_no' => $sponsor->license_data['dcci_no'] ?? null, + 'members' => $sponsor->license_data['members'] ?? [], + ]; + + $emiratesIdData = null; + if ($sponsor->emirates_id) { + $emiratesIdData = [ + 'emirates_id_number' => $sponsor->emirates_id, + 'name' => $sponsor->emirates_id_name, + 'nationality' => $sponsor->nationality, + 'date_of_birth' => $sponsor->emirates_id_dob, + 'expiry_date' => $sponsor->emirates_id_expiry, + 'issue_date' => $sponsor->emirates_id_issue_date, + 'employer' => $sponsor->emirates_id_employer, + 'issue_place' => $sponsor->emirates_id_issue_place, + 'occupation' => $sponsor->emirates_id_occupation, + ]; + } + + return [ + 'id' => $sponsor->id, + 'full_name' => $sponsor->full_name, + 'organization_name' => $sponsor->organization_name, + 'mobile' => $sponsor->mobile, + 'email' => $sponsor->email, + 'nationality' => $sponsor->nationality, + 'city' => $sponsor->city, + 'address' => $sponsor->address, + 'country_code' => $sponsor->country_code, + 'is_verified' => $sponsor->is_verified, + 'status' => $sponsor->status, + 'license_file' => $sponsor->license_file ? asset($sponsor->license_file) : null, + 'emirates_id_file' => $sponsor->emirates_id_file ? asset($sponsor->emirates_id_file) : null, + 'license_expiry' => $sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : null, + 'validity' => $sponsor->license_expiry ? ($sponsor->license_expiry->isFuture() ? 'Valid' : 'Expired') : 'Pending Review', + 'joined_at' => $sponsor->created_at->toIso8601String(), + 'fcm_token' => $sponsor->fcm_token, + 'license' => $licenseData, + 'emirates_id' => $emiratesIdData, + ]; + } + /** * Change sponsor's password. * @@ -394,4 +439,276 @@ public function changePassword(Request $request) ], 500); } } + + /** + * POST /api/sponsors/profile/update + * + * Updates the authenticated sponsor's profile. + */ + public function updateProfile(Request $request) + { + /** @var Sponsor $sponsor */ + $sponsor = $request->attributes->get('sponsor'); + + if (!$sponsor) { + return response()->json([ + 'success' => false, + 'message' => 'Unauthorized.' + ], 401); + } + + // Find matching employer user using sponsor's original email + $oldEmail = $sponsor->getOriginal('email') ?? $sponsor->email; + $matchingUser = \App\Models\User::where('email', $oldEmail) + ->where('role', 'employer') + ->first(); + + $matchingUserId = $matchingUser ? $matchingUser->id : 'NULL'; + + $validator = \Illuminate\Support\Facades\Validator::make($request->all(), [ + 'name' => 'required_without:full_name|nullable|string|max:255', + 'full_name' => 'required_without:name|nullable|string|max:255', + 'email' => 'required|email|max:255|unique:sponsors,email,' . $sponsor->id . '|unique:users,email,' . $matchingUserId, + 'mobile' => 'required|string|max:255|unique:sponsors,mobile,' . $sponsor->id, + 'organization_name' => 'required|string|max:255', + 'city' => 'required|string|max:100', + 'address' => 'required|string|max:255', + 'nationality' => 'required|string|max:100', + 'country_code' => 'required|string|max:10', + 'fcm_token' => 'nullable|string|max:255', + + // Emirates ID — accepts nested object OR flat fields + 'emirates_id' => 'nullable|array', + 'emirates_id.emirates_id_number' => 'nullable|string|max:255', + 'emirates_id.name' => 'nullable|string|max:255', + 'emirates_id.nationality' => 'nullable|string|max:255', + 'emirates_id.date_of_birth' => 'nullable|string|max:255', + 'emirates_id.expiry_date' => 'nullable|string|max:255', + 'emirates_id.issue_date' => 'nullable|string|max:255', + 'emirates_id.employer' => 'nullable|string|max:255', + 'emirates_id.issue_place' => 'nullable|string|max:255', + 'emirates_id.occupation' => 'nullable|string|max:255', + + // Flat emirates ID fields (backward compatibility) + 'id_number' => 'nullable|string|max:255', + 'card_number' => 'nullable|string|max:255', + 'date_of_birth' => 'nullable|string|max:255', + 'gender' => 'nullable|string|max:255', + 'issue_date' => 'nullable|string|max:255', + 'expiry_date' => 'nullable|string|max:255', + 'occupation' => 'nullable|string|max:255', + 'employer' => 'nullable|string|max:255', + 'issuing_place' => 'nullable|string|max:255', + + // License — accepts nested object OR flat fields + 'license' => 'nullable|array', + 'license.document_type' => 'nullable|string|max:255', + 'license.country' => 'nullable|string|max:255', + 'license.authority' => 'nullable|string|max:255', + 'license.license_no' => 'nullable|string|max:255', + 'license.company_name' => 'nullable|string|max:255', + 'license.business_name' => 'nullable|string|max:255', + 'license.license_category' => 'nullable|string|max:255', + 'license.legal_type' => 'nullable|string|max:255', + 'license.main_license_no' => 'nullable|string|max:255', + 'license.register_no' => 'nullable|string|max:255', + 'license.dcci_no' => 'nullable|string|max:255', + 'license.members' => 'nullable|array', + + // Flat license fields (backward compatibility) + 'document_type' => 'nullable|string|max:255', + 'country' => 'nullable|string|max:255', + 'authority' => 'nullable|string|max:255', + 'license_no' => 'nullable|string|max:255', + 'company_name' => 'nullable|string|max:255', + 'business_name' => 'nullable|string|max:255', + 'license_category' => 'nullable|string|max:255', + 'legal_type' => 'nullable|string|max:255', + 'main_license_no' => 'nullable|string|max:255', + 'register_no' => 'nullable|string|max:255', + 'dcci_no' => 'nullable|string|max:255', + 'members' => 'nullable|array', + ]); + + $validator->after(function ($validator) use ($request) { + $nameFromEid = $request->input('emirates_id.name') ?? $request->input('full_name'); + if ($request->has('full_name') && !$request->filled('full_name') && !$nameFromEid) { + $validator->errors()->add('full_name', 'Failed to extract name from the Emirates ID. Please upload a clearer document.'); + } + }); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Resolve nested emirates_id input into flat variables + $eidInput = $request->input('emirates_id'); + $eidNumber = null; + $eidName = null; + $eidDob = null; + $eidExpiry = null; + $eidIssueDate = null; + $eidEmployer = null; + $eidIssuePlace = null; + $eidOccupation = null; + $eidCardNumber = null; + $eidGender = null; + + if (is_array($eidInput)) { + $eidNumber = $eidInput['emirates_id_number'] ?? $eidInput['id_number'] ?? null; + $eidName = $eidInput['name'] ?? null; + $eidDob = $eidInput['date_of_birth'] ?? $eidInput['dob'] ?? null; + $eidExpiry = $eidInput['expiry_date'] ?? null; + $eidIssueDate = $eidInput['issue_date'] ?? null; + $eidEmployer = $eidInput['employer'] ?? null; + $eidIssuePlace = $eidInput['issue_place'] ?? $eidInput['issuing_place'] ?? null; + $eidOccupation = $eidInput['occupation'] ?? null; + $eidCardNumber = $eidInput['card_number'] ?? null; + $eidGender = $eidInput['gender'] ?? null; + } + + // Flat field fallbacks (backward compatibility) + $eidNumber = $eidNumber ?? $request->input('id_number'); + $eidName = $eidName ?? $request->input('full_name'); + $eidDob = $eidDob ?? $request->input('date_of_birth'); + $eidExpiry = $eidExpiry ?? $request->input('expiry_date'); + $eidIssueDate = $eidIssueDate ?? $request->input('issue_date'); + $eidEmployer = $eidEmployer ?? $request->input('employer'); + $eidIssuePlace = $eidIssuePlace ?? $request->input('issuing_place'); + $eidOccupation = $eidOccupation ?? $request->input('occupation'); + $eidCardNumber = $eidCardNumber ?? $request->input('card_number'); + $eidGender = $eidGender ?? $request->input('gender'); + + $nameToUse = $eidName ?? $request->name ?? $request->full_name; + + // Validate Emirates ID immutability — once set, it cannot be changed + if ($eidNumber) { + if ($sponsor->emirates_id && $sponsor->emirates_id !== $eidNumber) { + return response()->json([ + 'success' => false, + 'message' => 'Emirates ID mismatch. You cannot change your registered Emirates ID number.', + 'errors' => [ + 'id_number' => ['Emirates ID mismatch.'] + ] + ], 422); + } + } + + // Sync with corresponding employer user and profile records if found + if ($matchingUser) { + $userData = [ + 'name' => $nameToUse, + 'email' => $request->email, + ]; + if ($request->has('fcm_token')) { + $userData['fcm_token'] = $request->fcm_token; + } + $matchingUser->update($userData); + + $profile = $matchingUser->employerProfile; + if (!$profile) { + $profile = new \App\Models\EmployerProfile(['user_id' => $matchingUser->id]); + } + $profile->address = $request->address; + $profile->phone = $request->mobile; + + if ($eidNumber) { $profile->emirates_id_number = $eidNumber; } + if ($eidCardNumber) { $profile->emirates_id_card_number = $eidCardNumber; } + if ($eidName) { $profile->emirates_id_name = $eidName; } + if ($eidDob) { $profile->emirates_id_dob = $eidDob; } + if ($request->has('nationality')) { $profile->nationality = $request->nationality; } + if ($eidGender) { $profile->emirates_id_gender = $eidGender; } + if ($eidIssueDate) { $profile->emirates_id_issue_date = $eidIssueDate; } + if ($eidExpiry) { $profile->emirates_id_expiry = $eidExpiry; } + if ($eidOccupation) { $profile->emirates_id_occupation = $eidOccupation; } + if ($eidEmployer) { $profile->emirates_id_employer = $eidEmployer; } + if ($eidIssuePlace) { $profile->emirates_id_issue_place = $eidIssuePlace; } + + $profile->save(); + } + + // Update Sponsor profile + $sponsorData = [ + 'full_name' => $nameToUse, + 'email' => $request->email, + 'mobile' => $request->mobile, + 'organization_name' => $request->organization_name, + 'city' => $request->city, + 'address' => $request->address, + 'nationality' => $request->nationality, + 'country_code' => $request->country_code, + ]; + + if ($request->has('fcm_token')) { + $sponsorData['fcm_token'] = $request->fcm_token; + } + + if ($eidNumber) { $sponsorData['emirates_id'] = $eidNumber; } + if ($eidCardNumber) { $sponsorData['emirates_id_card_number'] = $eidCardNumber; } + if ($eidName) { $sponsorData['emirates_id_name'] = $eidName; } + if ($eidDob) { $sponsorData['emirates_id_dob'] = $eidDob; } + if ($eidGender) { $sponsorData['emirates_id_gender'] = $eidGender; } + if ($eidIssueDate) { $sponsorData['emirates_id_issue_date'] = $eidIssueDate; } + if ($eidExpiry) { $sponsorData['emirates_id_expiry'] = $eidExpiry; } + if ($eidOccupation) { $sponsorData['emirates_id_occupation'] = $eidOccupation; } + if ($eidEmployer) { $sponsorData['emirates_id_employer'] = $eidEmployer; } + if ($eidIssuePlace) { $sponsorData['emirates_id_issue_place'] = $eidIssuePlace; } + + // Parse and merge license data — from nested license object or flat fields + $currentLicenseData = is_array($sponsor->license_data) ? $sponsor->license_data : []; + $licenseInput = $request->input('license'); + $newLicenseData = []; + + if (is_array($licenseInput)) { + foreach ([ + 'document_type', 'country', 'authority', 'license_no', 'company_name', + 'business_name', 'license_category', 'legal_type', 'issue_date', 'expiry_date', + 'main_license_no', 'register_no', 'dcci_no', 'members' + ] as $field) { + if (isset($licenseInput[$field])) { + $newLicenseData[$field] = $licenseInput[$field]; + } + } + } + + // Flat field fallbacks (backward compatibility) + foreach ([ + 'document_type', 'country', 'authority', 'license_no', 'company_name', + 'business_name', 'license_category', 'legal_type', 'issue_date', 'expiry_date', + 'main_license_no', 'register_no', 'dcci_no', 'members' + ] as $field) { + if (!isset($newLicenseData[$field]) && $request->has($field)) { + $newLicenseData[$field] = $request->input($field); + } + } + + if (!empty($newLicenseData)) { + $sponsorData['license_data'] = array_merge($currentLicenseData, $newLicenseData); + } + + $sponsor->update($sponsorData); + + return response()->json([ + 'success' => true, + 'message' => 'Profile updated successfully.', + 'data' => [ + 'sponsor' => $this->buildSponsorResponse($sponsor) + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Sponsor Update Profile Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while updating profile.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } } diff --git a/app/Http/Controllers/Employer/EmployerAuthController.php b/app/Http/Controllers/Employer/EmployerAuthController.php index 87144c5..bb368dd 100644 --- a/app/Http/Controllers/Employer/EmployerAuthController.php +++ b/app/Http/Controllers/Employer/EmployerAuthController.php @@ -338,6 +338,14 @@ public function uploadEmiratesId(Request $request) $extractedIssueDate = $backOcr['issue_date'] ?? null; } + if (($request->hasFile('emirates_id_front') || $request->hasFile('emirates_id_file')) && empty($extractedName)) { + return response()->json([ + 'errors' => [ + 'emirates_id_front' => ['Failed to extract name from the Emirates ID. Please upload a clear image.'] + ] + ], 422); + } + $pending = session('pending_employer_registration'); $pending['emirates_id_file'] = null; $pending['emirates_id_number'] = $extractedIdNumber; @@ -351,6 +359,14 @@ public function uploadEmiratesId(Request $request) $pending['emirates_id_employer'] = $extractedEmployer; $pending['emirates_id_issue_place'] = $extractedIssuePlace; $pending['emirates_id_issue_date'] = $extractedIssueDate; + + if (!empty($extractedName)) { + if (isset($pending['company_name']) && (empty($pending['company_name']) || $pending['company_name'] === ($pending['name'] ?? '') . ' Household')) { + $pending['company_name'] = $extractedName . ' Household'; + } + $pending['name'] = $extractedName; + } + session(['pending_employer_registration' => $pending]); session(['employer_emirates_id_uploaded' => true]); diff --git a/app/Http/Controllers/Employer/ProfileController.php b/app/Http/Controllers/Employer/ProfileController.php index aee3269..aa03e47 100644 --- a/app/Http/Controllers/Employer/ProfileController.php +++ b/app/Http/Controllers/Employer/ProfileController.php @@ -170,6 +170,62 @@ public function update(Request $request) $oldEmail = $user->getOriginal('email') ?? $user->email; + // Update EmployerProfile + $profile = $user->employerProfile; + if (!$profile) { + $profile = new EmployerProfile(['user_id' => $user->id]); + } + + // Handle physical uploads/OCR for Emirates ID first to allow name autofill + $uploaded = false; + $extractedIdNumber = null; + $extractedExpiry = null; + $extractedName = null; + $extractedDob = null; + $extractedNationality = null; + $extractedCardNumber = null; + $extractedGender = null; + $extractedOccupation = null; + $extractedEmployer = null; + $extractedIssuePlace = null; + $extractedIssueDate = null; + + if ($request->hasFile('emirates_id_front')) { + $file = $request->file('emirates_id_front'); + $ocrFront = \App\Services\OcrDocumentService::extractEmiratesIdFrontData($file); + $extractedIdNumber = $ocrFront['emirates_id_number'] ?? null; + $extractedName = $ocrFront['name'] ?? null; + $extractedDob = $ocrFront['date_of_birth'] ?? null; + $extractedNationality = $ocrFront['nationality'] ?? null; + $extractedCardNumber = $ocrFront['card_number'] ?? null; + $extractedGender = $ocrFront['gender'] ?? null; + $extractedOccupation = $ocrFront['occupation'] ?? null; + $extractedEmployer = $ocrFront['employer'] ?? null; + $extractedIssuePlace = $ocrFront['issue_place'] ?? null; + $profile->emirates_id_front = '[DELETED_FOR_PDPL_COMPLIANCE]'; + $uploaded = true; + } + if ($request->hasFile('emirates_id_back')) { + $file = $request->file('emirates_id_back'); + $ocrBack = \App\Services\OcrDocumentService::extractEmiratesIdBackData($file); + $extractedExpiry = $ocrBack['expiry_date'] ?? null; + $extractedIssueDate = $ocrBack['issue_date'] ?? null; + $profile->emirates_id_back = '[DELETED_FOR_PDPL_COMPLIANCE]'; + $uploaded = true; + } + + if ($uploaded) { + // Validate that name was extracted + if ($request->hasFile('emirates_id_front') && empty($extractedName)) { + return back()->withErrors(['emirates_id_front' => 'Failed to extract name from the Emirates ID. Please upload a clear image.']); + } + + // Autofill user's name + if (!empty($extractedName)) { + $request->merge(['name' => $extractedName]); + } + } + // Update User Model $user->update([ 'name' => $request->name, @@ -179,18 +235,50 @@ public function update(Request $request) // Sync with corresponding sponsor record if found $matchingSponsor = \App\Models\Sponsor::where('email', $oldEmail)->first(); if ($matchingSponsor) { - $matchingSponsor->update([ + $sponsorData = [ 'full_name' => $request->name, 'email' => $request->email, 'mobile' => $request->phone, - ]); + ]; + + if ($uploaded) { + if ($extractedIdNumber) { + $sponsorData['emirates_id'] = $extractedIdNumber; + } + if ($extractedExpiry) { + $sponsorData['emirates_id_expiry'] = $extractedExpiry; + } + if ($extractedName) { + $sponsorData['emirates_id_name'] = $extractedName; + } + if ($extractedDob) { + $sponsorData['emirates_id_dob'] = $extractedDob; + } + if ($extractedNationality) { + $sponsorData['nationality'] = $extractedNationality; + } + if ($extractedGender) { + $sponsorData['emirates_id_gender'] = $extractedGender; + } + if ($extractedOccupation) { + $sponsorData['emirates_id_occupation'] = $extractedOccupation; + } + if ($extractedEmployer) { + $sponsorData['emirates_id_employer'] = $extractedEmployer; + } + if ($extractedIssuePlace) { + $sponsorData['emirates_id_issue_place'] = $extractedIssuePlace; + } + if ($extractedIssueDate) { + $sponsorData['emirates_id_issue_date'] = $extractedIssueDate; + } + if ($extractedCardNumber) { + $sponsorData['emirates_id_card_number'] = $extractedCardNumber; + } + } + $matchingSponsor->update($sponsorData); } - // Update EmployerProfile - $profile = $user->employerProfile; - if (!$profile) { - $profile = new EmployerProfile(['user_id' => $user->id]); - } $profile->phone = $request->phone; if ($request->has('company_name')) { @@ -202,7 +290,7 @@ public function update(Request $request) if ($request->has('notifications')) { $profile->notifications = $request->notifications; } - if ($request->has('nationality')) { + if ($request->has('nationality') && !$uploaded) { $profile->nationality = $request->nationality; } if ($request->has('family_size')) { @@ -227,35 +315,38 @@ public function update(Request $request) $profile->push_notifications = $request->push_notifications; } - // Handle physical uploads/OCR for Emirates ID - $uploaded = false; - $extractedIdNumber = null; - $extractedExpiry = null; - - if ($request->hasFile('emirates_id_front')) { - $file = $request->file('emirates_id_front'); - $ocrFront = \App\Services\OcrDocumentService::extractData($file); - $extractedIdNumber = $ocrFront['document_number']; - $extractedExpiry = $ocrFront['expiry_date']; - $profile->emirates_id_front = '[DELETED_FOR_PDPL_COMPLIANCE]'; - $uploaded = true; - } - if ($request->hasFile('emirates_id_back')) { - $file = $request->file('emirates_id_back'); - $ocrBack = \App\Services\OcrDocumentService::extractData($file); - if (empty($extractedIdNumber)) { - $extractedIdNumber = $ocrBack['document_number']; - } - if (empty($extractedExpiry)) { - $extractedExpiry = $ocrBack['expiry_date']; - } - $profile->emirates_id_back = '[DELETED_FOR_PDPL_COMPLIANCE]'; - $uploaded = true; - } - if ($uploaded) { - $profile->emirates_id_number = $extractedIdNumber ?? ('784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9)); - $profile->emirates_id_expiry = $extractedExpiry ?? now()->addYears(rand(2, 4))->toDateString(); + $profile->emirates_id_number = $extractedIdNumber ?? $profile->emirates_id_number ?? ('784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9)); + $profile->emirates_id_expiry = $extractedExpiry ?? $profile->emirates_id_expiry ?? now()->addYears(rand(2, 4))->toDateString(); + + if ($extractedName) { + $profile->emirates_id_name = $extractedName; + } + if ($extractedDob) { + $profile->emirates_id_dob = $extractedDob; + } + if ($extractedNationality) { + $profile->nationality = $extractedNationality; + } + if ($extractedCardNumber) { + $profile->emirates_id_card_number = $extractedCardNumber; + } + if ($extractedGender) { + $profile->emirates_id_gender = $extractedGender; + } + if ($extractedOccupation) { + $profile->emirates_id_occupation = $extractedOccupation; + } + if ($extractedEmployer) { + $profile->emirates_id_employer = $extractedEmployer; + } + if ($extractedIssuePlace) { + $profile->emirates_id_issue_place = $extractedIssuePlace; + } + if ($extractedIssueDate) { + $profile->emirates_id_issue_date = $extractedIssueDate; + } + $profile->verification_status = 'approved'; } diff --git a/app/Models/Sponsor.php b/app/Models/Sponsor.php index 76bdb8b..408a8bb 100644 --- a/app/Models/Sponsor.php +++ b/app/Models/Sponsor.php @@ -45,6 +45,7 @@ class Sponsor extends Authenticatable 'emirates_id_occupation', 'emirates_id_card_number', 'emirates_id_gender', + 'license_data', ]; protected $hidden = [ @@ -59,6 +60,7 @@ class Sponsor extends Authenticatable 'subscription_end_date' => 'datetime', 'last_login_at' => 'datetime', 'license_expiry' => 'datetime', + 'license_data' => 'array', ]; public function hasActiveSubscription(): bool @@ -66,4 +68,58 @@ public function hasActiveSubscription(): bool return $this->subscription_status === 'active' && ($this->subscription_end_date === null || $this->subscription_end_date->isFuture()); } + + /** + * Convert the model instance to an array. + * + * @return array + */ + public function toArray() + { + $array = parent::toArray(); + + // Remove any flat properties that should be nested + unset($array['license_data']); + + $licenseDataObj = [ + 'license_number' => $this->license_data['license_no'] ?? $this->license_data['license_number'] ?? null, + 'organization_name' => $this->organization_name, + 'expiry_date' => $this->license_expiry ? $this->license_expiry->toDateString() : ($this->license_data['expiry_date'] ?? null), + 'document_type' => $this->license_data['document_type'] ?? null, + 'country' => $this->license_data['country'] ?? null, + 'authority' => $this->license_data['authority'] ?? null, + 'license_no' => $this->license_data['license_no'] ?? null, + 'company_name' => $this->license_data['company_name'] ?? null, + 'business_name' => $this->license_data['business_name'] ?? null, + 'license_category' => $this->license_data['license_category'] ?? null, + 'legal_type' => $this->license_data['legal_type'] ?? null, + 'issue_date' => $this->license_data['issue_date'] ?? null, + 'main_license_no' => $this->license_data['main_license_no'] ?? null, + 'register_no' => $this->license_data['register_no'] ?? null, + 'dcci_no' => $this->license_data['dcci_no'] ?? null, + 'members' => $this->license_data['members'] ?? [], + ]; + + $emiratesIdData = null; + if ($this->emirates_id) { + $emiratesIdData = [ + 'emirates_id_number' => $this->emirates_id, + 'name' => $this->emirates_id_name, + 'nationality' => $this->nationality, + 'date_of_birth' => $this->emirates_id_dob, + 'expiry_date' => $this->emirates_id_expiry, + 'issue_date' => $this->emirates_id_issue_date, + 'employer' => $this->emirates_id_employer, + 'issue_place' => $this->emirates_id_issue_place, + 'occupation' => $this->emirates_id_occupation, + 'card_number' => $this->emirates_id_card_number, + 'gender' => $this->emirates_id_gender, + ]; + } + + return array_merge($array, [ + 'license' => $licenseDataObj, + 'emirates_id' => $emiratesIdData, + ]); + } } diff --git a/database/migrations/2026_06_24_124100_add_license_data_to_sponsors_table.php b/database/migrations/2026_06_24_124100_add_license_data_to_sponsors_table.php new file mode 100644 index 0000000..ddaf9d3 --- /dev/null +++ b/database/migrations/2026_06_24_124100_add_license_data_to_sponsors_table.php @@ -0,0 +1,32 @@ +json('license_data')->nullable()->after('license_expiry'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('sponsors', function (Blueprint $table) { + if (Schema::hasColumn('sponsors', 'license_data')) { + $table->dropColumn('license_data'); + } + }); + } +}; diff --git a/public/swagger.json b/public/swagger.json index 284d8cb..08a0094 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -74,6 +74,78 @@ "expiry_date": { "type": "string", "example": "2028-06-12" + }, + "document_type": { + "type": "string", + "example": "dubai_commercial_license" + }, + "country": { + "type": "string", + "example": "United Arab Emirates" + }, + "authority": { + "type": "string", + "example": "Dubai Economy and Tourism" + }, + "license_no": { + "type": "string", + "example": "828302" + }, + "company_name": { + "type": "string", + "example": "MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C" + }, + "business_name": { + "type": "string", + "example": "MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C" + }, + "license_category": { + "type": "string", + "example": "Dep. of Economic Development" + }, + "legal_type": { + "type": "string", + "example": "Limited Liability Company Single Owner(LLC- - SO)" + }, + "issue_date": { + "type": "string", + "example": "06/03/2019" + }, + "main_license_no": { + "type": "string", + "example": "828302" + }, + "register_no": { + "type": "string", + "example": "1656486" + }, + "dcci_no": { + "type": "string", + "example": "317412" + }, + "members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "person_no": { + "type": "string", + "example": "752967" + }, + "name": { + "type": "string", + "example": "Lo lo wallus issued in" + }, + "nationality": { + "type": "string", + "example": "India" + }, + "role": { + "type": "string", + "example": "Shares Owner Who" + } + } + } } } }, @@ -586,7 +658,28 @@ "description": "Returns the full profile of the authenticated sponsor.", "responses": { "200": { - "description": "Profile retrieved successfully." + "description": "Profile retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "sponsor": { + "$ref": "#/components/schemas/Sponsor" + } + } + } + } + } + } + } }, "401": { "description": "Unauthenticated." @@ -594,6 +687,248 @@ } } }, + "/sponsors/profile/update": { + "post": { + "tags": [ + "Sponsor" + ], + "summary": "Update Sponsor Profile Details", + "description": "Updates the authenticated sponsor's profile details including optional Emirates ID metadata. Password changes are not supported via this endpoint.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email", + "mobile", + "organization_name", + "city", + "address", + "nationality", + "country_code" + ], + "properties": { + "name": { + "type": "string", + "example": "Ahmad Bin Ahmed", + "description": "Sponsor's full name (required if full_name is not provided)" + }, + "full_name": { + "type": "string", + "example": "Ahmad Bin Ahmed", + "description": "Sponsor's full name (required if name is not provided)" + }, + "email": { + "type": "string", + "format": "email", + "example": "test.sponsor@example.com" + }, + "mobile": { + "type": "string", + "example": "+971509990001" + }, + "organization_name": { + "type": "string", + "example": "Charity Co" + }, + "city": { + "type": "string", + "example": "Abu Dhabi" + }, + "address": { + "type": "string", + "example": "Villa 45, Street 12" + }, + "nationality": { + "type": "string", + "example": "Emirati" + }, + "country_code": { + "type": "string", + "example": "+971" + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example", + "description": "Firebase Cloud Messaging token for push notifications." + }, + "license": { + "type": "object", + "description": "Optional JSON object containing extracted commercial license details.", + "properties": { + "license_number": { + "type": "string", + "example": "828302" + }, + "organization_name": { + "type": "string", + "example": "MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C" + }, + "expiry_date": { + "type": "string", + "example": "05/03/2026" + }, + "document_type": { + "type": "string", + "example": "dubai_commercial_license" + }, + "country": { + "type": "string", + "example": "United Arab Emirates" + }, + "authority": { + "type": "string", + "example": "Dubai Economy and Tourism" + }, + "license_no": { + "type": "string", + "example": "828302" + }, + "company_name": { + "type": "string", + "example": "MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C" + }, + "business_name": { + "type": "string", + "example": "MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C" + }, + "license_category": { + "type": "string", + "example": "Dep. of Economic Development" + }, + "legal_type": { + "type": "string", + "example": "Limited Liability Company Single Owner(LLC- - SO)" + }, + "issue_date": { + "type": "string", + "example": "06/03/2019" + }, + "main_license_no": { + "type": "string", + "example": "828302" + }, + "register_no": { + "type": "string", + "example": "1656486" + }, + "dcci_no": { + "type": "string", + "example": "317412" + }, + "members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "person_no": { + "type": "string", + "example": "752967" + }, + "name": { + "type": "string", + "example": "Lo lo wallus issued in" + }, + "nationality": { + "type": "string", + "example": "India" + }, + "role": { + "type": "string", + "example": "Shares Owner Who" + } + } + } + } + } + }, + "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" + } + } + } + } + } + } + } + }, + "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": { + "sponsor": { + "$ref": "#/components/schemas/Sponsor" + } + } + } + } + } + } + } + }, + "401": { + "description": "Unauthenticated." + }, + "422": { + "description": "Validation error (duplicate mobile/email, mismatched immutable fields, or name extraction failure)." + } + } + } + }, "/workers/register": { "post": { "tags": [ @@ -6251,6 +6586,131 @@ "type": "string", "example": "fcm_token_example" }, + "document_type": { + "type": "string", + "example": "dubai_commercial_license" + }, + "country": { + "type": "string", + "example": "United Arab Emirates" + }, + "authority": { + "type": "string", + "example": "Dubai Economy and Tourism" + }, + "license_no": { + "type": "string", + "example": "828302" + }, + "company_name": { + "type": "string", + "example": "MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C" + }, + "business_name": { + "type": "string", + "example": "MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C" + }, + "license_category": { + "type": "string", + "example": "Dep. of Economic Development" + }, + "legal_type": { + "type": "string", + "example": "Limited Liability Company Single Owner(LLC- - SO)" + }, + "issue_date": { + "type": "string", + "example": "06/03/2019" + }, + "expiry_date": { + "type": "string", + "example": "05/03/2026" + }, + "main_license_no": { + "type": "string", + "example": "828302" + }, + "register_no": { + "type": "string", + "example": "1656486" + }, + "dcci_no": { + "type": "string", + "example": "317412" + }, + "members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "person_no": { + "type": "string", + "example": "752967" + }, + "name": { + "type": "string", + "example": "Lo lo wallus issued in" + }, + "nationality": { + "type": "string", + "example": "India" + }, + "role": { + "type": "string", + "example": "Shares Owner Who" + } + } + } + }, + "emirates_id": { + "type": "object", + "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" + }, + "card_number": { + "type": "string", + "example": "987654321" + }, + "gender": { + "type": "string", + "example": "Male" + } + } + }, "created_at": { "type": "string", "format": "date-time" diff --git a/resources/js/Pages/Employer/Auth/UploadEmiratesId.jsx b/resources/js/Pages/Employer/Auth/UploadEmiratesId.jsx index 0d19dfd..15bea9e 100644 --- a/resources/js/Pages/Employer/Auth/UploadEmiratesId.jsx +++ b/resources/js/Pages/Employer/Auth/UploadEmiratesId.jsx @@ -72,9 +72,10 @@ export default function UploadEmiratesId({ email }) { } catch (err) { if (err.response && err.response.data && err.response.data.errors) { const errors = err.response.data.errors; - const errorMsg = errors.emirates_id_front?.[0] || errors.emirates_id_back?.[0] || 'Validation failed.'; + const firstError = Object.values(errors).flat()[0]; + const errorMsg = firstError || 'Validation failed.'; setError(errorMsg); - toast.error('Validation failed. Please verify the document.'); + toast.error(errorMsg); } else { toast.error('Scan extraction failed. Please try again.'); } diff --git a/routes/api.php b/routes/api.php index c38459d..06b9ba6 100644 --- a/routes/api.php +++ b/routes/api.php @@ -159,6 +159,7 @@ // Sponsors can ONLY access: dashboard and charity events. No payments, no worker browsing. Route::middleware(['auth.sponsor'])->group(function () { Route::get('/sponsors/profile', [SponsorController::class, 'getProfile']); + Route::post('/sponsors/profile/update', [SponsorController::class, 'updateProfile']); Route::get('/sponsors/dashboard', [SponsorController::class, 'getDashboard']); Route::get('/sponsors/charity-events', [SponsorController::class, 'getCharityEvents']); Route::post('/sponsors/charity-events', [SponsorController::class, 'postCharityEvent']); diff --git a/tests/Feature/EmployerProfileApiTest.php b/tests/Feature/EmployerProfileApiTest.php index 6ca7a7b..e7d3bc6 100644 --- a/tests/Feature/EmployerProfileApiTest.php +++ b/tests/Feature/EmployerProfileApiTest.php @@ -141,7 +141,7 @@ public function test_employer_can_update_profile_with_emirates_id_fields() 'success' => true, 'data' => [ 'profile' => [ - 'name' => 'Ahmad', + 'name' => 'Mohammad Jobaier Mohammad Abul Kalam', 'email' => 'ahmad@example.com', 'phone' => '+971509990001', 'address' => 'Villa 12, Jumeirah 2', diff --git a/tests/Feature/FcmAuthenticationTest.php b/tests/Feature/FcmAuthenticationTest.php index e5c7b96..e5410ff 100644 --- a/tests/Feature/FcmAuthenticationTest.php +++ b/tests/Feature/FcmAuthenticationTest.php @@ -54,6 +54,7 @@ public function test_employer_registration_and_login_stores_fcm_token_and_sends_ 'fcm_token' => 'employer_register_fcm_token', 'emirates_id' => [ 'emirates_id_number' => '784-1988-5310327-2', + 'name' => 'Employer FCM Test', 'expiry_date' => '2028-04-11', ] ]); @@ -182,6 +183,7 @@ public function test_sponsor_registration_and_login_stores_fcm_token_and_sends_n ], 'emirates_id' => [ 'emirates_id_number' => '784-1988-5310327-2', + 'name' => 'Sponsor FCM Org', ], 'organization_name' => 'Sponsor FCM Co', 'email' => 'sponsor.fcm@example.com', diff --git a/tests/Feature/SponsorAuthApiTest.php b/tests/Feature/SponsorAuthApiTest.php index 9b28dbb..28fc0bb 100644 --- a/tests/Feature/SponsorAuthApiTest.php +++ b/tests/Feature/SponsorAuthApiTest.php @@ -71,7 +71,7 @@ public function test_sponsor_can_register_with_license() $this->assertDatabaseHas('sponsors', [ 'mobile' => '+971509990001', - 'full_name' => 'Test Sponsor Organization', + 'full_name' => 'Ahmad Bin Ahmed', 'license_expiry' => '2028-06-12 00:00:00', 'emirates_id_name' => 'Ahmad Bin Ahmed', 'emirates_id_dob' => '1990-01-01', @@ -537,6 +537,7 @@ public function test_sponsor_registration_with_emirates_id_and_separate_license( 'country_code' => '+971', 'emirates_id' => [ 'emirates_id_number' => '784-1988-5310327-2', + 'name' => 'Test Sponsor Sep', ], ]); @@ -657,4 +658,229 @@ public function test_sponsor_can_change_password() $this->assertTrue(Hash::check('NewPassword@123', $sponsor->fresh()->password)); $this->assertTrue(Hash::check('NewPassword@123', $user->fresh()->password)); } + + /** + * Test sponsor can update profile and it correctly syncs with matching user/employer profile. + */ + public function test_sponsor_can_update_profile_and_syncs_with_matching_user() + { + $sponsor = Sponsor::create([ + 'full_name' => 'Original Sponsor Name', + 'email' => 'sync-test@example.com', + 'mobile' => '+971500000001', + 'password' => Hash::make('Password@123'), + 'organization_name' => 'Original Org', + 'city' => 'Dubai', + 'address' => 'Old Address', + 'nationality' => 'Emirati', + 'country_code' => '+971', + 'api_token' => 'sync-test-token', + 'status' => 'active', + ]); + + $user = User::create([ + 'name' => 'Original Sponsor Name', + 'email' => 'sync-test@example.com', + 'password' => Hash::make('Password@123'), + 'role' => 'employer', + ]); + + $profile = EmployerProfile::create([ + 'user_id' => $user->id, + 'company_name' => 'Original Org', + 'phone' => '+971500000001', + 'address' => 'Old Address', + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer sync-test-token', + ])->postJson('/api/sponsors/profile/update', [ + 'name' => 'Updated Sponsor Name', + 'email' => 'new-sync-test@example.com', + 'mobile' => '+971500000002', + 'organization_name' => 'Updated Org', + 'city' => 'Abu Dhabi', + 'address' => 'New Address', + 'nationality' => 'Jordanian', + 'country_code' => '+962', + 'id_number' => '784-2000-1234567-1', + 'card_number' => '987654321', + 'full_name' => 'Updated Sponsor Name', + 'date_of_birth' => '2000-01-01', + 'gender' => 'Male', + 'issue_date' => '2023-01-01', + 'expiry_date' => '2028-01-01', + 'occupation' => 'Business Owner', + 'employer' => 'Self', + 'issuing_place' => 'Abu Dhabi', + 'document_type' => 'dubai_commercial_license', + 'country' => 'United Arab Emirates', + 'authority' => 'Dubai Economy and Tourism', + 'license_no' => '828302', + 'company_name' => 'MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C', + 'business_name' => 'MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C', + 'license_category' => 'Dep. of Economic Development', + 'legal_type' => 'Limited Liability Company Single Owner(LLC- - SO)', + 'main_license_no' => '828302', + 'register_no' => '1656486', + 'dcci_no' => '317412', + 'members' => [ + [ + 'person_no' => '752967', + 'name' => 'Lo lo wallus issued in', + 'nationality' => 'India', + 'role' => 'Shares Owner Who' + ] + ] + ]); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'message' => 'Profile updated successfully.', + ]) + ->assertJsonPath('data.sponsor.full_name', 'Updated Sponsor Name') + ->assertJsonPath('data.sponsor.email', 'new-sync-test@example.com') + ->assertJsonPath('data.sponsor.mobile', '+971500000002') + ->assertJsonPath('data.sponsor.organization_name', 'Updated Org') + ->assertJsonPath('data.sponsor.city', 'Abu Dhabi') + ->assertJsonPath('data.sponsor.address', 'New Address') + ->assertJsonPath('data.sponsor.nationality', 'Jordanian') + ->assertJsonPath('data.sponsor.country_code', '+962') + ->assertJsonPath('data.sponsor.license.document_type', 'dubai_commercial_license') + ->assertJsonPath('data.sponsor.license.license_no', '828302') + ->assertJsonPath('data.sponsor.license.members.0.name', 'Lo lo wallus issued in') + ->assertJsonPath('data.sponsor.emirates_id.emirates_id_number', '784-2000-1234567-1') + ->assertJsonPath('data.sponsor.emirates_id.name', 'Updated Sponsor Name') + ->assertJsonPath('data.sponsor.emirates_id.occupation', 'Business Owner') + ->assertJsonPath('data.sponsor.emirates_id.employer', 'Self'); + + // Check Sponsor table + $this->assertDatabaseHas('sponsors', [ + 'id' => $sponsor->id, + 'full_name' => 'Updated Sponsor Name', + 'email' => 'new-sync-test@example.com', + 'mobile' => '+971500000002', + 'emirates_id' => '784-2000-1234567-1', + 'emirates_id_card_number' => '987654321', + 'emirates_id_name' => 'Updated Sponsor Name', + 'emirates_id_dob' => '2000-01-01', + 'emirates_id_gender' => 'Male', + 'emirates_id_issue_date' => '2023-01-01', + 'emirates_id_expiry' => '2028-01-01', + 'emirates_id_occupation' => 'Business Owner', + 'emirates_id_employer' => 'Self', + 'emirates_id_issue_place' => 'Abu Dhabi', + ]); + + $dbSponsor = Sponsor::find($sponsor->id); + $this->assertEquals('dubai_commercial_license', $dbSponsor->license_data['document_type']); + $this->assertEquals('Lo lo wallus issued in', $dbSponsor->license_data['members'][0]['name']); + + // Check User table + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'name' => 'Updated Sponsor Name', + 'email' => 'new-sync-test@example.com', + ]); + + // Check EmployerProfile table + $this->assertDatabaseHas('employer_profiles', [ + 'id' => $profile->id, + 'address' => 'New Address', + 'phone' => '+971500000002', + 'emirates_id_number' => '784-2000-1234567-1', + 'emirates_id_card_number' => '987654321', + 'emirates_id_name' => 'Updated Sponsor Name', + 'emirates_id_dob' => '2000-01-01', + 'emirates_id_gender' => 'Male', + 'emirates_id_issue_date' => '2023-01-01', + 'emirates_id_expiry' => '2028-01-01', + 'emirates_id_occupation' => 'Business Owner', + 'emirates_id_employer' => 'Self', + 'emirates_id_issue_place' => 'Abu Dhabi', + ]); + } + + /** + * Test sponsor cannot change immutable Emirates ID. + */ + public function test_sponsor_cannot_change_immutable_emirates_id() + { + $sponsor = Sponsor::create([ + 'full_name' => 'Sponsor Name', + 'email' => 'immutable-id-test@example.com', + 'mobile' => '+971500000003', + 'password' => Hash::make('Password@123'), + 'organization_name' => 'Org', + 'city' => 'Dubai', + 'address' => 'Address', + 'nationality' => 'Emirati', + 'country_code' => '+971', + 'api_token' => 'immutable-id-token', + 'status' => 'active', + 'emirates_id' => '784-2000-1234567-1', + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer immutable-id-token', + ])->postJson('/api/sponsors/profile/update', [ + 'name' => 'Sponsor Name', + 'email' => 'immutable-id-test@example.com', + 'mobile' => '+971500000003', + 'organization_name' => 'Org', + 'city' => 'Dubai', + 'address' => 'Address', + 'nationality' => 'Emirati', + 'country_code' => '+971', + 'id_number' => '784-2000-7654321-2', // different EID + ]); + + $response->assertStatus(422) + ->assertJson([ + 'success' => false, + 'message' => 'Emirates ID mismatch. You cannot change your registered Emirates ID number.', + ]) + ->assertJsonValidationErrors(['id_number']); + } + + /** + * Test sponsor profile update validates Emirates ID name OCR. + */ + public function test_sponsor_profile_update_validates_emirates_id_name_ocr() + { + $sponsor = Sponsor::create([ + 'full_name' => 'Sponsor Name', + 'email' => 'ocr-test@example.com', + 'mobile' => '+971500000004', + 'password' => Hash::make('Password@123'), + 'organization_name' => 'Org', + 'city' => 'Dubai', + 'address' => 'Address', + 'nationality' => 'Emirati', + 'country_code' => '+971', + 'api_token' => 'ocr-token', + 'status' => 'active', + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ocr-token', + ])->postJson('/api/sponsors/profile/update', [ + 'name' => 'Sponsor Name', + 'email' => 'ocr-test@example.com', + 'mobile' => '+971500000004', + 'organization_name' => 'Org', + 'city' => 'Dubai', + 'address' => 'Address', + 'nationality' => 'Emirati', + 'country_code' => '+971', + 'id_number' => '784-2000-1234567-1', + 'full_name' => '', // Failed OCR name extraction + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['full_name']) + ->assertJsonPath('errors.full_name.0', 'Failed to extract name from the Emirates ID. Please upload a clearer document.'); + } } + diff --git a/tests/Feature/SponsorAuthWebTest.php b/tests/Feature/SponsorAuthWebTest.php index ab7cc90..66a41b0 100644 --- a/tests/Feature/SponsorAuthWebTest.php +++ b/tests/Feature/SponsorAuthWebTest.php @@ -80,11 +80,12 @@ public function test_employer_web_registration_with_emirates_id() $this->assertDatabaseHas('users', [ 'email' => 'abdullah@example.com', 'role' => 'employer', + 'name' => 'Mohammad Jobaier Mohammad Abul Kalam', ]); $this->assertDatabaseHas('employer_profiles', [ 'phone' => '501234567', - 'company_name' => 'Abdullah Ahmed Household', + 'company_name' => 'Mohammad Jobaier Mohammad Abul Kalam Household', 'emirates_id_front' => null, // File is not stored 'address' => 'Villa 14, Al Safa, Dubai', ]); @@ -106,6 +107,7 @@ public function test_employer_web_registration_with_emirates_id() 'emirates_id_file' => null, // File is not stored 'address' => 'Villa 14, Al Safa, Dubai', 'emirates_id' => '784-1987-5493842-5', + 'full_name' => 'Mohammad Jobaier Mohammad Abul Kalam', 'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam', 'emirates_id_dob' => '1987-04-14', 'nationality' => 'Bangladesh', @@ -184,6 +186,19 @@ public function test_employer_web_registration_with_two_sided_emirates_id() $this->assertDatabaseHas('users', [ 'email' => 'fatima@example.com', 'role' => 'employer', + 'name' => 'Mohammad Jobaier Mohammad Abul Kalam', + ]); + + $this->assertDatabaseHas('employer_profiles', [ + 'phone' => '509876543', + 'company_name' => 'Mohammad Jobaier Mohammad Abul Kalam Household', + 'address' => 'Apartment 402, Al Nahda, Sharjah', + ]); + + $this->assertDatabaseHas('sponsors', [ + 'email' => 'fatima@example.com', + 'mobile' => '509876543', + 'full_name' => 'Mohammad Jobaier Mohammad Abul Kalam', ]); $profile = EmployerProfile::where('phone', '509876543')->first();