From 121337eea7dd2faf629cfed612da82dca7f51c6e Mon Sep 17 00:00:00 2001 From: mohanmd Date: Fri, 19 Jun 2026 10:53:04 +0530 Subject: [PATCH] api changes for register empyer, worker, sponsor --- .../Api/EmployerAuthController.php | 177 ++---- .../Api/GoogleVisionOcrController.php | 46 ++ .../Controllers/Api/SponsorAuthController.php | 164 ++---- app/Models/EmployerProfile.php | 9 +- app/Models/Sponsor.php | 8 + app/Services/GoogleVisionOcrService.php | 323 +++++++++++ config/services.php | 4 + ...0000_add_emirates_id_to_sponsors_table.php | 30 + ...s_id_fields_to_employer_profiles_table.php | 40 ++ ...a_emirates_id_fields_to_sponsors_table.php | 42 ++ ...add_address_to_employer_profiles_table.php | 28 + public/swagger.json | 538 +++++++----------- routes/api.php | 6 +- tests/Feature/EmployerMessageApiTest.php | 123 +--- tests/Feature/FcmAuthenticationTest.php | 23 +- tests/Feature/GoogleVisionOcrApiTest.php | 105 ++++ tests/Feature/SponsorAuthApiTest.php | 82 ++- vite.config.js | 2 +- 18 files changed, 988 insertions(+), 762 deletions(-) create mode 100644 app/Http/Controllers/Api/GoogleVisionOcrController.php create mode 100644 app/Services/GoogleVisionOcrService.php create mode 100644 database/migrations/2026_06_18_170000_add_emirates_id_to_sponsors_table.php create mode 100644 database/migrations/2026_06_18_215416_add_extra_emirates_id_fields_to_employer_profiles_table.php create mode 100644 database/migrations/2026_06_18_220109_add_extra_emirates_id_fields_to_sponsors_table.php create mode 100644 database/migrations/2026_06_18_220348_add_address_to_employer_profiles_table.php create mode 100644 tests/Feature/GoogleVisionOcrApiTest.php diff --git a/app/Http/Controllers/Api/EmployerAuthController.php b/app/Http/Controllers/Api/EmployerAuthController.php index d066c4b..43ff95a 100644 --- a/app/Http/Controllers/Api/EmployerAuthController.php +++ b/app/Http/Controllers/Api/EmployerAuthController.php @@ -191,6 +191,8 @@ public function register(Request $request) 'email' => 'required|string|email|max:255|unique:users,email|unique:sponsors,email', 'phone' => 'required|string|max:50|unique:employer_profiles,phone|unique:sponsors,mobile', 'fcm_token' => 'nullable|string|max:255', + 'emirates_id' => 'nullable|array', + 'address' => 'nullable|string|max:255', ], [ 'email.unique' => 'This email address is already registered.', 'phone.unique' => 'This mobile number is already registered.', @@ -215,6 +217,29 @@ public function register(Request $request) 'expires_at' => now()->addMinutes(10)->timestamp ], 600); + $emiratesIdInput = $request->input('emirates_id'); + $emiratesIdNumber = null; + $emiratesIdExpiry = null; + $emiratesIdName = null; + $emiratesIdDob = null; + $emiratesIdNationality = null; + $emiratesIdIssueDate = null; + $emiratesIdEmployer = null; + $emiratesIdIssuePlace = null; + $emiratesIdOccupation = null; + + if (is_array($emiratesIdInput)) { + $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; + $emiratesIdDob = $emiratesIdInput['date_of_birth'] ?? $emiratesIdInput['date of birth'] ?? $emiratesIdInput['dob'] ?? null; + $emiratesIdNationality = $emiratesIdInput['nationality'] ?? null; + $emiratesIdIssueDate = $emiratesIdInput['issue_date'] ?? $emiratesIdInput['issue date'] ?? null; + $emiratesIdEmployer = $emiratesIdInput['employer'] ?? null; + $emiratesIdIssuePlace = $emiratesIdInput['issue_place'] ?? $emiratesIdInput['issue place'] ?? null; + $emiratesIdOccupation = $emiratesIdInput['occupation'] ?? $emiratesIdInput['occuption'] ?? null; + } + // Create inactive User $user = User::create([ 'name' => $request->name, @@ -235,6 +260,16 @@ public function register(Request $request) 'verification_status' => 'pending', 'language' => 'English', 'notifications' => true, + 'emirates_id_number' => $emiratesIdNumber, + 'emirates_id_expiry' => $emiratesIdExpiry, + 'emirates_id_name' => $emiratesIdName, + 'emirates_id_dob' => $emiratesIdDob, + 'nationality' => $emiratesIdNationality, + 'emirates_id_issue_date' => $emiratesIdIssueDate, + 'emirates_id_employer' => $emiratesIdEmployer, + 'emirates_id_issue_place' => $emiratesIdIssuePlace, + 'emirates_id_occupation' => $emiratesIdOccupation, + 'address' => $request->address, ]); // Create unverified Sponsor @@ -253,6 +288,8 @@ public function register(Request $request) 'otp_verified_at' => null, 'status' => 'inactive', 'fcm_token' => $request->fcm_token, + 'emirates_id' => $emiratesIdNumber, + 'address' => $request->address, ]); if ($request->filled('fcm_token')) { @@ -723,144 +760,4 @@ public function resetPassword(Request $request) ], 500); } } - - /** - * POST /api/employers/register/emirates-front - */ - public function uploadEmiratesIdFront(Request $request) - { - $validator = Validator::make($request->all(), [ - 'email' => 'required|string|email|max:255', - 'emirates_id_front' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240', - ], [ - 'email.required' => 'The email address is required.', - 'emirates_id_front.required' => 'Please upload the front side of your Emirates ID.', - 'emirates_id_front.mimes' => 'The Emirates ID front must be an image or a PDF.', - ]); - - if ($validator->fails()) { - return response()->json([ - 'success' => false, - 'message' => 'Validation error.', - 'errors' => $validator->errors() - ], 422); - } - - try { - $user = User::where('email', $request->email)->first(); - $sponsor = \App\Models\Sponsor::where('email', $request->email)->first(); - - if (!$user) { - return response()->json([ - 'success' => false, - 'message' => 'User account not found.' - ], 404); - } - - $frontFile = $request->file('emirates_id_front'); - $frontOcr = \App\Services\OcrDocumentService::extractEmiratesIdFrontData($frontFile); - $extractedIdNumber = $frontOcr['emirates_id_number']; - - // Update or create EmployerProfile - $profile = EmployerProfile::where('user_id', $user->id)->first(); - if (!$profile) { - $profile = EmployerProfile::create([ - 'user_id' => $user->id, - 'company_name' => $user->name . ' Household', - 'phone' => $sponsor ? $sponsor->mobile : '+971 50 123 4567', - 'country' => 'United Arab Emirates', - 'verification_status' => 'pending', - ]); - } - - $profile->update([ - 'emirates_id_number' => $extractedIdNumber, - ]); - - return response()->json([ - 'success' => true, - 'message' => 'Emirates ID front uploaded and processed successfully.', - 'ocr_extracted_data' => $frontOcr, - 'email' => $request->email, - ], 200); - - } catch (\Exception $e) { - logger()->error('API Emirates ID Front Upload Failure: ' . $e->getMessage()); - return response()->json([ - 'success' => false, - 'message' => 'An error occurred while uploading the Emirates ID front.', - 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' - ], 500); - } - } - - /** - * POST /api/employers/register/emirates-back - */ - public function uploadEmiratesIdBack(Request $request) - { - $validator = Validator::make($request->all(), [ - 'email' => 'required|string|email|max:255', - 'emirates_id_back' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240', - ], [ - 'email.required' => 'The email address is required.', - 'emirates_id_back.required' => 'Please upload the back side of your Emirates ID.', - 'emirates_id_back.mimes' => 'The Emirates ID back must be an image or a PDF.', - ]); - - if ($validator->fails()) { - return response()->json([ - 'success' => false, - 'message' => 'Validation error.', - 'errors' => $validator->errors() - ], 422); - } - - try { - $user = User::where('email', $request->email)->first(); - $sponsor = \App\Models\Sponsor::where('email', $request->email)->first(); - - if (!$user) { - return response()->json([ - 'success' => false, - 'message' => 'User account not found.' - ], 404); - } - - $backFile = $request->file('emirates_id_back'); - $backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($backFile); - $extractedExpiry = $backOcr['expiry_date']; - - // Update or create EmployerProfile - $profile = EmployerProfile::where('user_id', $user->id)->first(); - if (!$profile) { - $profile = EmployerProfile::create([ - 'user_id' => $user->id, - 'company_name' => $user->name . ' Household', - 'phone' => $sponsor ? $sponsor->mobile : '+971 50 123 4567', - 'country' => 'United Arab Emirates', - 'verification_status' => 'pending', - ]); - } - - $profile->update([ - 'emirates_id_expiry' => $extractedExpiry, - ]); - - return response()->json([ - 'success' => true, - 'message' => 'Emirates ID back uploaded and processed successfully.', - 'ocr_extracted_data' => $backOcr, - 'email' => $request->email, - ], 200); - - } catch (\Exception $e) { - logger()->error('API Emirates ID Back Upload Failure: ' . $e->getMessage()); - return response()->json([ - 'success' => false, - 'message' => 'An error occurred while uploading the Emirates ID back.', - 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' - ], 500); - } - } } diff --git a/app/Http/Controllers/Api/GoogleVisionOcrController.php b/app/Http/Controllers/Api/GoogleVisionOcrController.php new file mode 100644 index 0000000..5d103b3 --- /dev/null +++ b/app/Http/Controllers/Api/GoogleVisionOcrController.php @@ -0,0 +1,46 @@ +all(), [ + 'passport_file' => 'required|file|image|max:10240', // Max 10MB + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors(), + ], 422); + } + + $file = $request->file('passport_file'); + + $result = GoogleVisionOcrService::extractPassportData($file); + + if (!$result['success']) { + return response()->json([ + 'success' => false, + 'message' => $result['message'] ?? 'Could not extract data from passport image.', + 'data' => $result['data'] ?? null, + ], 400); + } + + return response()->json([ + 'success' => true, + 'data' => $result['data'], + ], 200); + } +} diff --git a/app/Http/Controllers/Api/SponsorAuthController.php b/app/Http/Controllers/Api/SponsorAuthController.php index 23f905c..a5e2b49 100644 --- a/app/Http/Controllers/Api/SponsorAuthController.php +++ b/app/Http/Controllers/Api/SponsorAuthController.php @@ -26,8 +26,8 @@ public function register(Request $request) 'full_name' => 'required|string|max:255', 'mobile' => 'required|string|max:50|unique:sponsors,mobile|unique:employer_profiles,phone', 'password' => 'required|string|min:6', - 'license_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240', - 'emirates_id_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240', + 'license' => 'nullable|array', + 'emirates_id' => 'nullable|array', 'organization_name' => 'required|string|max:255', 'email' => 'required|email|max:255|unique:sponsors,email|unique:users,email', 'nationality' => 'required|string|max:100', @@ -58,22 +58,44 @@ public function register(Request $request) } $licenseExpiry = $request->license_expiry; - if ($request->hasFile('license_file')) { - $licenseFile = $request->file('license_file'); - $ocrLicense = \App\Services\OcrDocumentService::extractSponsorLicenseData($licenseFile); - $licenseExpiry = $licenseExpiry ?? $ocrLicense['expiry_date']; + $licenseInput = $request->input('license'); + if (is_array($licenseInput)) { + $licenseExpiry = $licenseExpiry ?? ($licenseInput['expiry_date'] ?? $licenseInput['license_expiry'] ?? null); + } + + $emiratesIdInput = $request->input('emirates_id'); + $emiratesId = null; + $emiratesIdExpiry = null; + $emiratesIdName = null; + $emiratesIdDob = null; + $emiratesIdNationality = null; + $emiratesIdIssueDate = null; + $emiratesIdEmployer = null; + $emiratesIdIssuePlace = null; + $emiratesIdOccupation = null; + + if (is_array($emiratesIdInput)) { + $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; + $emiratesIdDob = $emiratesIdInput['date_of_birth'] ?? $emiratesIdInput['date of birth'] ?? $emiratesIdInput['dob'] ?? null; + $emiratesIdNationality = $emiratesIdInput['nationality'] ?? null; + $emiratesIdIssueDate = $emiratesIdInput['issue_date'] ?? $emiratesIdInput['issue date'] ?? null; + $emiratesIdEmployer = $emiratesIdInput['employer'] ?? null; + $emiratesIdIssuePlace = $emiratesIdInput['issue_place'] ?? $emiratesIdInput['issue place'] ?? null; + $emiratesIdOccupation = $emiratesIdInput['occupation'] ?? $emiratesIdInput['occuption'] ?? null; } $emiratesIdPath = null; - if ($request->hasFile('emirates_id_file')) { - $emiratesIdFile = $request->file('emirates_id_file'); - \App\Services\OcrDocumentService::extractEmiratesIdFrontData($emiratesIdFile); - } $licensePath = null; $apiToken = Str::random(80); - $sponsor = DB::transaction(function () use ($request, $email, $licensePath, $emiratesIdPath, $apiToken, $licenseExpiry) { + $sponsor = DB::transaction(function () use ( + $request, $email, $licensePath, $emiratesIdPath, $emiratesId, $apiToken, $licenseExpiry, + $emiratesIdExpiry, $emiratesIdName, $emiratesIdDob, $emiratesIdIssueDate, $emiratesIdEmployer, + $emiratesIdIssuePlace, $emiratesIdOccupation + ) { return Sponsor::create([ 'full_name' => $request->full_name, 'organization_name' => $request->organization_name, @@ -86,12 +108,20 @@ public function register(Request $request) 'address' => $request->address, 'license_file' => $licensePath, 'emirates_id_file' => $emiratesIdPath, + 'emirates_id' => $emiratesId, 'license_expiry' => $licenseExpiry, 'status' => 'active', 'is_verified' => false, 'subscription_status' => 'none', 'api_token' => $apiToken, 'fcm_token' => $request->fcm_token, + 'emirates_id_name' => $emiratesIdName, + 'emirates_id_dob' => $emiratesIdDob, + 'emirates_id_issue_date' => $emiratesIdIssueDate, + 'emirates_id_expiry' => $emiratesIdExpiry, + 'emirates_id_employer' => $emiratesIdEmployer, + 'emirates_id_issue_place' => $emiratesIdIssuePlace, + 'emirates_id_occupation' => $emiratesIdOccupation, ]); }); @@ -189,119 +219,7 @@ public function login(Request $request) ], 200); } - /** - * Upload Sponsor's Emirates ID Front. - * POST /api/sponsors/register/emirates-front - */ - public function uploadEmiratesIdFront(Request $request) - { - $validator = Validator::make($request->all(), [ - 'email' => 'required|string|email|max:255', - 'emirates_id_front' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240', - ], [ - 'email.required' => 'The email address is required.', - 'emirates_id_front.required' => 'Please upload the front side of your Emirates ID.', - 'emirates_id_front.mimes' => 'The Emirates ID front must be an image or a PDF.', - ]); - if ($validator->fails()) { - return response()->json([ - 'success' => false, - 'message' => 'Validation error.', - 'errors' => $validator->errors() - ], 422); - } - - try { - $sponsor = Sponsor::where('email', $request->email)->first(); - - if (!$sponsor) { - return response()->json([ - 'success' => false, - 'message' => 'Sponsor account not found.' - ], 404); - } - - $frontFile = $request->file('emirates_id_front'); - $frontOcr = \App\Services\OcrDocumentService::extractEmiratesIdFrontData($frontFile); - - $sponsor->update([ - 'emirates_id_file' => null, - ]); - - return response()->json([ - 'success' => true, - 'message' => 'Emirates ID front uploaded and processed successfully.', - 'ocr_extracted_data' => $frontOcr, - 'email' => $request->email, - ], 200); - - } catch (\Exception $e) { - logger()->error('API Sponsor Emirates ID Front Upload Failure: ' . $e->getMessage()); - return response()->json([ - 'success' => false, - 'message' => 'An error occurred while uploading the Emirates ID front.', - 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' - ], 500); - } - } - - /** - * Upload Sponsor's Emirates ID Back. - * POST /api/sponsors/register/emirates-back - */ - public function uploadEmiratesIdBack(Request $request) - { - $validator = Validator::make($request->all(), [ - 'email' => 'required|string|email|max:255', - 'emirates_id_back' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240', - ], [ - 'email.required' => 'The email address is required.', - 'emirates_id_back.required' => 'Please upload the back side of your Emirates ID.', - 'emirates_id_back.mimes' => 'The Emirates ID back must be an image or a PDF.', - ]); - - if ($validator->fails()) { - return response()->json([ - 'success' => false, - 'message' => 'Validation error.', - 'errors' => $validator->errors() - ], 422); - } - - try { - $sponsor = Sponsor::where('email', $request->email)->first(); - - if (!$sponsor) { - return response()->json([ - 'success' => false, - 'message' => 'Sponsor account not found.' - ], 404); - } - - $backFile = $request->file('emirates_id_back'); - $backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($backFile); - - $sponsor->update([ - 'emirates_id_file' => null, - ]); - - return response()->json([ - 'success' => true, - 'message' => 'Emirates ID back uploaded and processed successfully.', - 'ocr_extracted_data' => $backOcr, - 'email' => $request->email, - ], 200); - - } catch (\Exception $e) { - logger()->error('API Sponsor Emirates ID Back Upload Failure: ' . $e->getMessage()); - return response()->json([ - 'success' => false, - 'message' => 'An error occurred while uploading the Emirates ID back.', - 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' - ], 500); - } - } /** * Upload Sponsor's License document. diff --git a/app/Models/EmployerProfile.php b/app/Models/EmployerProfile.php index 3fbadef..094ae88 100644 --- a/app/Models/EmployerProfile.php +++ b/app/Models/EmployerProfile.php @@ -22,6 +22,13 @@ class EmployerProfile extends Model 'accommodation', 'district', 'emirates_id_number', - 'emirates_id_expiry' + 'emirates_id_expiry', + 'emirates_id_name', + 'emirates_id_dob', + 'emirates_id_issue_date', + 'emirates_id_employer', + 'emirates_id_issue_place', + 'emirates_id_occupation', + 'address' ]; } diff --git a/app/Models/Sponsor.php b/app/Models/Sponsor.php index 71f3bf3..e624722 100644 --- a/app/Models/Sponsor.php +++ b/app/Models/Sponsor.php @@ -33,8 +33,16 @@ class Sponsor extends Authenticatable 'api_token', 'license_file', 'emirates_id_file', + 'emirates_id', 'license_expiry', 'fcm_token', + 'emirates_id_name', + 'emirates_id_dob', + 'emirates_id_issue_date', + 'emirates_id_expiry', + 'emirates_id_employer', + 'emirates_id_issue_place', + 'emirates_id_occupation', ]; protected $hidden = [ diff --git a/app/Services/GoogleVisionOcrService.php b/app/Services/GoogleVisionOcrService.php new file mode 100644 index 0000000..1537b69 --- /dev/null +++ b/app/Services/GoogleVisionOcrService.php @@ -0,0 +1,323 @@ + false, + 'message' => 'Google Vision API key is not configured.', + 'data' => self::emptyData(), + ]; + } + + try { + $base64Image = base64_encode($file->get()); + + $url = "https://vision.googleapis.com/v1/images:annotate?key={$apiKey}"; + + $payload = [ + 'requests' => [ + [ + 'image' => [ + 'content' => $base64Image, + ], + 'features' => [ + [ + 'type' => 'TEXT_DETECTION', + ], + ], + ], + ], + ]; + + $response = Http::withoutVerifying()->timeout(30)->post($url, $payload); + + if (!$response->successful()) { + Log::error('Google Vision API error response: ' . $response->body()); + return [ + 'success' => false, + 'message' => 'Google Vision API request failed.', + 'data' => self::emptyData(), + ]; + } + + $json = $response->json(); + $fullText = $json['responses'][0]['fullTextAnnotation']['text'] ?? ''; + + if (empty($fullText)) { + Log::warning('Google Vision API did not detect any text.'); + return [ + 'success' => true, + 'data' => self::emptyData(), + ]; + } + + $parsedData = self::parsePassportText($fullText); + return [ + 'success' => true, + 'data' => $parsedData, + ]; + + } catch (\Exception $e) { + Log::error('Google Vision OCR exception: ' . $e->getMessage()); + return [ + 'success' => false, + 'message' => 'An error occurred during OCR processing.', + 'data' => self::emptyData(), + ]; + } + } + + private static function emptyData(): array + { + return [ + 'date_of_birth' => null, + 'date_of_expiry' => null, + 'date_of_issue' => null, + 'given_names' => null, + 'issuing_country' => null, + 'nationality' => null, + 'passport_number' => null, + 'place_of_birth' => null, + ]; + } + + /** + * Parse raw OCR text to extract passport fields. + */ + public static function parsePassportText(string $text): array + { + $data = self::emptyData(); + + // Split text into lines + $lines = explode("\n", $text); + $cleanLines = []; + foreach ($lines as $line) { + $line = trim($line); + if ($line !== '') { + $cleanLines[] = $line; + } + } + + // Try to parse MRZ first + $mrzLines = self::findMrzLines($cleanLines); + if ($mrzLines) { + $mrzData = self::parseMrz($mrzLines[0], $mrzLines[1]); + foreach ($mrzData as $key => $val) { + if ($val !== null) { + $data[$key] = $val; + } + } + } + + // ── Passport Number fallback ───────────────────────────────────────── + if (empty($data['passport_number'])) { + foreach ($cleanLines as $line) { + if (preg_match('/(?:passport|pass\s*no|doc\s*no|document|number|no\.?)\s*[:\-\s]\s*([A-Z0-9]{7,9})/i', $line, $matches)) { + $data['passport_number'] = strtoupper($matches[1]); + break; + } + } + if (empty($data['passport_number'])) { + foreach ($cleanLines as $line) { + if (preg_match('/\b([A-Z][0-9]{7,8}|[A-Z0-9]{8,9})\b/', $line, $matches)) { + $data['passport_number'] = strtoupper($matches[1]); + break; + } + } + } + } + + // ── Given Names fallback ───────────────────────────────────────────── + if (empty($data['given_names'])) { + foreach ($cleanLines as $line) { + if (preg_match('/(?:given\s*names?|first\s*names?|name|nom)\s*[:\-\s]\s*([A-Z\s]{2,})/i', $line, $matches)) { + $data['given_names'] = trim(strtoupper($matches[1])); + break; + } + } + } + + // ── Place of Birth fallback ────────────────────────────────────────── + if (empty($data['place_of_birth'])) { + foreach ($cleanLines as $line) { + if (preg_match('/(?:place\s*of\s*birth|lieu\s*de\s*naissance|birthplace)\s*[:\-\s]\s*([A-Z\s]{2,})/i', $line, $matches)) { + $data['place_of_birth'] = trim(strtoupper($matches[1])); + break; + } + } + } + + // ── Dates fallback ─────────────────────────────────────────────────── + if (empty($data['date_of_birth'])) { + $data['date_of_birth'] = self::findDateByKeyword($cleanLines, ['birth', 'dob', 'naissance']); + } + if (empty($data['date_of_expiry'])) { + $data['date_of_expiry'] = self::findDateByKeyword($cleanLines, ['expiry', 'expiration', 'exp', 'valable']); + } + if (empty($data['date_of_issue'])) { + $data['date_of_issue'] = self::findDateByKeyword($cleanLines, ['issue', 'delivrance', 'issued']); + } + + // ── Nationality fallback ───────────────────────────────────────────── + if (empty($data['nationality'])) { + foreach ($cleanLines as $line) { + if (preg_match('/(?:nationality|nationalite)\s*[:\-\s]\s*([A-Z\s]{3,})/i', $line, $matches)) { + $data['nationality'] = trim(strtoupper($matches[1])); + break; + } + } + } + + // ── Issuing Country fallback ───────────────────────────────────────── + if (empty($data['issuing_country'])) { + foreach ($cleanLines as $line) { + if (preg_match('/(?:issuing\s*state|issuing\s*country|country)\s*[:\-\s]\s*([A-Z\s]{3,})/i', $line, $matches)) { + $data['issuing_country'] = trim(strtoupper($matches[1])); + break; + } + } + } + + return $data; + } + + /** + * Look for MRZ lines in clean lines. + */ + private static function findMrzLines(array $lines): ?array + { + $candidateLines = []; + foreach ($lines as $line) { + $cleaned = str_replace(' ', '', $line); + $cleaned = str_replace('«', '<', $cleaned); + if (strlen($cleaned) >= 35 && strlen($cleaned) <= 50) { + $candidateLines[] = [ + 'original' => $line, + 'cleaned' => $cleaned + ]; + } + } + + for ($i = 0; $i < count($candidateLines); $i++) { + $c1 = $candidateLines[$i]['cleaned']; + if (strpos($c1, 'P') === 0) { + for ($j = 0; $j < count($candidateLines); $j++) { + if ($i === $j) continue; + $c2 = $candidateLines[$j]['cleaned']; + if (preg_match('/^[A-Z0-9<]{35,50}$/', $c2)) { + if (preg_match('/\d{6}/', $c2)) { + return [$candidateLines[$i]['cleaned'], $candidateLines[$j]['cleaned']]; + } + } + } + } + } + + return null; + } + + /** + * Parse standard 44-character passport MRZ lines. + */ + private static function parseMrz(string $line1, string $line2): array + { + $data = self::emptyData(); + + $line1 = str_pad(substr($line1, 0, 44), 44, '<'); + $line2 = str_pad(substr($line2, 0, 44), 44, '<'); + + $data['issuing_country'] = str_replace('<', '', substr($line1, 2, 3)); + + $namePart = substr($line1, 5); + $parts = explode('<<', $namePart); + $surname = isset($parts[0]) ? str_replace('<', ' ', $parts[0]) : ''; + $givenNames = isset($parts[1]) ? str_replace('<', ' ', $parts[1]) : ''; + + $data['given_names'] = trim(strtoupper($givenNames)); + if (empty($data['given_names'])) { + $data['given_names'] = trim(strtoupper($surname)); + } + + $data['passport_number'] = str_replace('<', '', substr($line2, 0, 9)); + $data['nationality'] = str_replace('<', '', substr($line2, 10, 3)); + + $rawDob = substr($line2, 13, 6); + if (ctype_digit($rawDob)) { + $data['date_of_birth'] = self::formatMrzDate($rawDob, true); + } + + $rawExpiry = substr($line2, 21, 6); + if (ctype_digit($rawExpiry)) { + $data['date_of_expiry'] = self::formatMrzDate($rawExpiry, false); + } + + return $data; + } + + private static function formatMrzDate(string $yymmdd, bool $isBirth): string + { + $yy = intval(substr($yymmdd, 0, 2)); + $mm = substr($yymmdd, 2, 2); + $dd = substr($yymmdd, 4, 2); + + $currentYear = intval(date('Y')); + $currentYearShort = $currentYear % 100; + + if ($isBirth) { + $year = ($yy > $currentYearShort) ? (1900 + $yy) : (2000 + $yy); + } else { + $year = ($yy > ($currentYearShort + 20)) ? (1900 + $yy) : (2000 + $yy); + } + + return "{$dd}/{$mm}/{$year}"; + } + + private static function findDateByKeyword(array $lines, array $keywords): ?string + { + foreach ($lines as $i => $line) { + $matched = false; + foreach ($keywords as $kw) { + if (stripos($line, $kw) !== false) { + $matched = true; + break; + } + } + + if ($matched) { + $textToSearch = $line; + if (isset($lines[$i + 1])) { + $textToSearch .= ' ' . $lines[$i + 1]; + } + + if (preg_match('/(\d{1,2})[\/\-\.](\d{1,2})[\/\-\.](\d{4})/', $textToSearch, $matches)) { + $day = str_pad($matches[1], 2, '0', STR_PAD_LEFT); + $month = str_pad($matches[2], 2, '0', STR_PAD_LEFT); + $year = $matches[3]; + return "{$day}/{$month}/{$year}"; + } + if (preg_match('/(\d{4})[\/\-\.](\d{1,2})[\/\-\.](\d{1,2})/', $textToSearch, $matches)) { + $year = $matches[1]; + $month = str_pad($matches[2], 2, '0', STR_PAD_LEFT); + $day = str_pad($matches[3], 2, '0', STR_PAD_LEFT); + return "{$day}/{$month}/{$year}"; + } + } + } + + return null; + } +} diff --git a/config/services.php b/config/services.php index 808553f..5e77493 100644 --- a/config/services.php +++ b/config/services.php @@ -43,4 +43,8 @@ 'sponsor_license_url' => env('SPONSOR_LICENSE_API_URL'), ], + 'google' => [ + 'vision_api_key' => env('GOOGLE_VISION_API_KEY'), + ], + ]; diff --git a/database/migrations/2026_06_18_170000_add_emirates_id_to_sponsors_table.php b/database/migrations/2026_06_18_170000_add_emirates_id_to_sponsors_table.php new file mode 100644 index 0000000..4c3434a --- /dev/null +++ b/database/migrations/2026_06_18_170000_add_emirates_id_to_sponsors_table.php @@ -0,0 +1,30 @@ +string('emirates_id')->nullable()->after('emirates_id_file'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('sponsors', function (Blueprint $table) { + $table->dropColumn('emirates_id'); + }); + } +}; diff --git a/database/migrations/2026_06_18_215416_add_extra_emirates_id_fields_to_employer_profiles_table.php b/database/migrations/2026_06_18_215416_add_extra_emirates_id_fields_to_employer_profiles_table.php new file mode 100644 index 0000000..5aa825d --- /dev/null +++ b/database/migrations/2026_06_18_215416_add_extra_emirates_id_fields_to_employer_profiles_table.php @@ -0,0 +1,40 @@ +string('emirates_id_name')->nullable(); + $table->string('emirates_id_dob')->nullable(); + $table->string('emirates_id_issue_date')->nullable(); + $table->string('emirates_id_employer')->nullable(); + $table->string('emirates_id_issue_place')->nullable(); + $table->string('emirates_id_occupation')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('employer_profiles', function (Blueprint $table) { + $table->dropColumn([ + 'emirates_id_name', + 'emirates_id_dob', + 'emirates_id_issue_date', + 'emirates_id_employer', + 'emirates_id_issue_place', + 'emirates_id_occupation', + ]); + }); + } +}; diff --git a/database/migrations/2026_06_18_220109_add_extra_emirates_id_fields_to_sponsors_table.php b/database/migrations/2026_06_18_220109_add_extra_emirates_id_fields_to_sponsors_table.php new file mode 100644 index 0000000..c5f9c16 --- /dev/null +++ b/database/migrations/2026_06_18_220109_add_extra_emirates_id_fields_to_sponsors_table.php @@ -0,0 +1,42 @@ +string('emirates_id_name')->nullable(); + $table->string('emirates_id_dob')->nullable(); + $table->string('emirates_id_issue_date')->nullable(); + $table->string('emirates_id_expiry')->nullable(); + $table->string('emirates_id_employer')->nullable(); + $table->string('emirates_id_issue_place')->nullable(); + $table->string('emirates_id_occupation')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('sponsors', function (Blueprint $table) { + $table->dropColumn([ + 'emirates_id_name', + 'emirates_id_dob', + 'emirates_id_issue_date', + 'emirates_id_expiry', + 'emirates_id_employer', + 'emirates_id_issue_place', + 'emirates_id_occupation', + ]); + }); + } +}; diff --git a/database/migrations/2026_06_18_220348_add_address_to_employer_profiles_table.php b/database/migrations/2026_06_18_220348_add_address_to_employer_profiles_table.php new file mode 100644 index 0000000..7664c63 --- /dev/null +++ b/database/migrations/2026_06_18_220348_add_address_to_employer_profiles_table.php @@ -0,0 +1,28 @@ +string('address')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('employer_profiles', function (Blueprint $table) { + $table->dropColumn('address'); + }); + } +}; diff --git a/public/swagger.json b/public/swagger.json index acd5b93..ca30fed 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -28,7 +28,7 @@ "requestBody": { "required": true, "content": { - "multipart/form-data": { + "application/json": { "schema": { "type": "object", "required": [ @@ -51,7 +51,7 @@ "mobile": { "type": "string", "example": "+971501112233", - "description": "Mobile phone number \u2014 must be unique. (REQUIRED)" + "description": "Mobile phone number — must be unique. (REQUIRED)" }, "password": { "type": "string", @@ -59,15 +59,65 @@ "example": "securepass123", "description": "Login password (min 6 characters). (REQUIRED)" }, - "license_file": { - "type": "string", - "format": "binary", - "description": "Organization or trade license document (jpg/png/pdf, max 10MB). (REQUIRED)" + "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_file": { - "type": "string", - "format": "binary", - "description": "Emirates ID document file (jpg/png/pdf, max 10MB). (REQUIRED)" + "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", @@ -155,194 +205,6 @@ } } }, - "/sponsors/register/emirates-front": { - "post": { - "tags": [ - "Sponsor/Auth" - ], - "summary": "Upload Sponsor Emirates ID Front", - "description": "Uploads and processes the front side of a Sponsor's Emirates ID using OCR.", - "security": [], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": [ - "email", - "emirates_id_front" - ], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "info@alrashidi.ae", - "description": "Email address of the registered sponsor. (REQUIRED)" - }, - "emirates_id_front": { - "type": "string", - "format": "binary", - "description": "Emirates ID front image/PDF. (REQUIRED)" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Emirates ID front processed successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Emirates ID front uploaded and processed successfully." - }, - "ocr_extracted_data": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "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" - }, - "ocr_accuracy": { - "type": "number", - "example": 98.5 - } - } - }, - "email": { - "type": "string", - "example": "info@alrashidi.ae" - } - } - } - } - } - }, - "404": { - "description": "Sponsor account not found." - }, - "422": { - "description": "Validation error." - } - } - } - }, - "/sponsors/register/emirates-back": { - "post": { - "tags": [ - "Sponsor/Auth" - ], - "summary": "Upload Sponsor Emirates ID Back", - "description": "Uploads and processes the back side of a Sponsor's Emirates ID using OCR.", - "security": [], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": [ - "email", - "emirates_id_back" - ], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "info@alrashidi.ae", - "description": "Email address of the registered sponsor. (REQUIRED)" - }, - "emirates_id_back": { - "type": "string", - "format": "binary", - "description": "Emirates ID back image/PDF. (REQUIRED)" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Emirates ID back processed successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Emirates ID back uploaded and processed successfully." - }, - "ocr_extracted_data": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "expiry_date": { - "type": "string", - "example": "2028-04-11" - }, - "issue_date": { - "type": "string", - "example": "2023-04-11" - }, - "ocr_accuracy": { - "type": "number", - "example": 98.5 - } - } - }, - "email": { - "type": "string", - "example": "info@alrashidi.ae" - } - } - } - } - } - }, - "404": { - "description": "Sponsor account not found." - }, - "422": { - "description": "Validation error." - } - } - } - }, "/sponsors/register/license": { "post": { "tags": [ @@ -1140,6 +1002,97 @@ } } }, + "/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": [ @@ -2857,10 +2810,57 @@ "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." } } } @@ -2919,148 +2919,6 @@ } } }, - "/employers/register/emirates-front": { - "post": { - "tags": [ - "Employer/Auth" - ], - "summary": "Upload Emirates ID Front Side Scan & OCR Extract", - "description": "Accepts an email and an Emirates ID front scan file, performs automatic OCR extraction of the Emirates ID number, updates the employer's profile fields, and returns the extracted front data.", - "security": [], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": [ - "email", - "emirates_id_front" - ], - "properties": { - "email": { - "type": "string", - "example": "ahmad@example.com", - "description": "Employer's email address." - }, - "emirates_id_front": { - "type": "string", - "format": "binary", - "description": "Physical scan or image file of the Emirates ID front side (Max 10MB)." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Emirates ID front uploaded and processed successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Emirates ID front uploaded and processed successfully." - }, - "ocr_extracted_data": { - "type": "object", - "properties": { - "emirates_id_number": { - "type": "string", - "example": "784-1988-5310327-2" - } - } - }, - "email": { - "type": "string", - "example": "ahmad@example.com" - } - } - } - } - } - } - } - } - }, - "/employers/register/emirates-back": { - "post": { - "tags": [ - "Employer/Auth" - ], - "summary": "Upload Emirates ID Back Side Scan & OCR Extract", - "description": "Accepts an email and an Emirates ID back scan file, performs automatic OCR extraction of the card expiry date, updates the employer's profile fields, and returns the extracted back data.", - "security": [], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": [ - "email", - "emirates_id_back" - ], - "properties": { - "email": { - "type": "string", - "example": "ahmad@example.com", - "description": "Employer's email address." - }, - "emirates_id_back": { - "type": "string", - "format": "binary", - "description": "Physical scan or image file of the Emirates ID back side (Max 10MB)." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Emirates ID back uploaded and processed successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Emirates ID back uploaded and processed successfully." - }, - "ocr_extracted_data": { - "type": "object", - "properties": { - "expiry_date": { - "type": "string", - "example": "2028-04-11" - } - } - }, - "email": { - "type": "string", - "example": "ahmad@example.com" - } - } - } - } - } - } - } - } - }, "/employers/payment": { "post": { "tags": [ diff --git a/routes/api.php b/routes/api.php index 439e6f3..ad74825 100644 --- a/routes/api.php +++ b/routes/api.php @@ -12,6 +12,7 @@ use App\Http\Controllers\Api\EmployerReviewController; use App\Http\Controllers\Api\SponsorAuthController; use App\Http\Controllers\Api\SponsorController; +use App\Http\Controllers\Api\GoogleVisionOcrController; /* |-------------------------------------------------------------------------- @@ -42,12 +43,11 @@ Route::post('/workers/setup-profile', [WorkerAuthController::class, 'setupProfile']); Route::post('/workers/register', [WorkerAuthController::class, 'register']); Route::post('/workers/login', [WorkerAuthController::class, 'login']); +Route::post('/workers/ocr/passport-vision', [GoogleVisionOcrController::class, 'extractPassport']); // Unprotected Employer Mobile Auth Endpoints Route::post('/employers/register', [EmployerAuthController::class, 'register']); Route::post('/employers/verify', [EmployerAuthController::class, 'verify']); -Route::post('/employers/register/emirates-front', [EmployerAuthController::class, 'uploadEmiratesIdFront']); -Route::post('/employers/register/emirates-back', [EmployerAuthController::class, 'uploadEmiratesIdBack']); Route::post('/employers/payment', [EmployerAuthController::class, 'payment']); Route::post('/employers/password', [EmployerAuthController::class, 'password']); Route::get('/employers/plans', [EmployerAuthController::class, 'plans']); @@ -57,8 +57,6 @@ // Sponsor Mobile Auth Endpoints (unprotected) Route::post('/sponsors/register', [SponsorAuthController::class, 'register']); -Route::post('/sponsors/register/emirates-front', [SponsorAuthController::class, 'uploadEmiratesIdFront']); -Route::post('/sponsors/register/emirates-back', [SponsorAuthController::class, 'uploadEmiratesIdBack']); Route::post('/sponsors/register/license', [SponsorAuthController::class, 'uploadLicense']); Route::post('/sponsors/login', [EmployerAuthController::class, 'login']); diff --git a/tests/Feature/EmployerMessageApiTest.php b/tests/Feature/EmployerMessageApiTest.php index 663497f..fbca86c 100644 --- a/tests/Feature/EmployerMessageApiTest.php +++ b/tests/Feature/EmployerMessageApiTest.php @@ -89,9 +89,6 @@ public function test_employer_can_login() */ public function test_employer_can_register() { - \Illuminate\Support\Facades\Storage::fake('local'); - $fakeIdFile = \Illuminate\Http\UploadedFile::fake()->create('emirates_id.jpg', 800, 'image/jpeg'); - // Step 1: Register $response = $this->postJson('/api/employers/register', [ 'company_name' => 'New Company Corp', @@ -100,6 +97,18 @@ public function test_employer_can_register() 'phone' => '+971500000003', 'password' => 'Password@123', 'password_confirmation' => 'Password@123', + 'address' => 'Test Address 123', + 'emirates_id' => [ + 'emirates_id_number' => '784-1988-5310327-2', + 'expiry_date' => '2028-04-11', + 'name' => 'Alice Employer', + 'date_of_birth' => '1990-01-01', + 'nationality' => 'Emirati', + 'issue_date' => '2023-04-11', + 'employer' => 'Federal Authority', + 'issue_place' => 'Abu Dhabi', + 'occuption' => 'Manager' + ] ]); $response->assertStatus(201) @@ -116,105 +125,23 @@ public function test_employer_can_register() $this->assertDatabaseHas('employer_profiles', [ 'company_name' => 'Alice Employer Household', + 'emirates_id_number' => '784-1988-5310327-2', + 'emirates_id_expiry' => '2028-04-11', + 'emirates_id_name' => 'Alice Employer', + 'emirates_id_dob' => '1990-01-01', + 'nationality' => 'Emirati', + 'emirates_id_issue_date' => '2023-04-11', + 'emirates_id_employer' => 'Federal Authority', + 'emirates_id_issue_place' => 'Abu Dhabi', + 'emirates_id_occupation' => 'Manager', + 'address' => 'Test Address 123', ]); - // Step 2: Upload Emirates ID Front - $uploadFrontResponse = $this->postJson('/api/employers/register/emirates-front', [ + $this->assertDatabaseHas('sponsors', [ 'email' => 'alice@example.com', - 'emirates_id_front' => $fakeIdFile, + 'emirates_id' => '784-1988-5310327-2', + 'address' => 'Test Address 123', ]); - - $uploadFrontResponse->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'message', - 'ocr_extracted_data' => [ - 'emirates_id_number', - ], - 'email' - ]); - - // Step 3: Upload Emirates ID Back - $uploadBackResponse = $this->postJson('/api/employers/register/emirates-back', [ - 'email' => 'alice@example.com', - 'emirates_id_back' => $fakeIdFile, - ]); - - $uploadBackResponse->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'message', - 'ocr_extracted_data' => [ - 'expiry_date', - ], - 'email' - ]); - - $user = User::where('email', 'alice@example.com')->first(); - $profile = EmployerProfile::where('user_id', $user->id)->first(); - - $this->assertNull($profile->emirates_id_front); - $this->assertNotNull($profile->emirates_id_number); - $this->assertNotNull($profile->emirates_id_expiry); - } - - /** - * Test employer can upload front and back side Emirates ID separately via distinct API endpoints. - */ - public function test_employer_can_upload_separate_emirates_id_front_and_back() - { - \Illuminate\Support\Facades\Storage::fake('local'); - $frontFile = \Illuminate\Http\UploadedFile::fake()->create('front.jpg', 500, 'image/jpeg'); - $backFile = \Illuminate\Http\UploadedFile::fake()->create('back.jpg', 500, 'image/jpeg'); - - // Create an employer user to upload for - $user = User::create([ - 'name' => 'Charlie Employer', - 'email' => 'charlie@example.com', - 'password' => bcrypt('password'), - 'role' => 'employer', - ]); - - // Upload Front - $responseFront = $this->postJson('/api/employers/register/emirates-front', [ - 'email' => 'charlie@example.com', - 'emirates_id_front' => $frontFile, - ]); - - $responseFront->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'message', - 'ocr_extracted_data' => [ - 'emirates_id_number', - ], - 'email' - ]); - - $this->assertEquals('784-1988-5310327-2', $responseFront->json('ocr_extracted_data.emirates_id_number')); - - // Upload Back - $responseBack = $this->postJson('/api/employers/register/emirates-back', [ - 'email' => 'charlie@example.com', - 'emirates_id_back' => $backFile, - ]); - - $responseBack->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'message', - 'ocr_extracted_data' => [ - 'expiry_date', - ], - 'email' - ]); - - $this->assertEquals('2028-04-11', $responseBack->json('ocr_extracted_data.expiry_date')); - - $profile = EmployerProfile::where('user_id', $user->id)->first(); - $this->assertNotNull($profile); - $this->assertEquals('784-1988-5310327-2', $profile->emirates_id_number); - $this->assertEquals('2028-04-11', $profile->emirates_id_expiry); } /** diff --git a/tests/Feature/FcmAuthenticationTest.php b/tests/Feature/FcmAuthenticationTest.php index 2d0e9df..e5c7b96 100644 --- a/tests/Feature/FcmAuthenticationTest.php +++ b/tests/Feature/FcmAuthenticationTest.php @@ -51,7 +51,11 @@ public function test_employer_registration_and_login_stores_fcm_token_and_sends_ 'name' => 'Employer FCM Test', 'email' => 'employer.fcm@example.com', 'phone' => '+971500000001', - 'fcm_token' => 'employer_register_fcm_token' + 'fcm_token' => 'employer_register_fcm_token', + 'emirates_id' => [ + 'emirates_id_number' => '784-1988-5310327-2', + 'expiry_date' => '2028-04-11', + ] ]); $response->assertStatus(201); @@ -59,6 +63,10 @@ public function test_employer_registration_and_login_stores_fcm_token_and_sends_ $user = User::where('email', 'employer.fcm@example.com')->first(); $this->assertEquals('employer_register_fcm_token', $user->fcm_token); + $profile = EmployerProfile::where('user_id', $user->id)->first(); + $this->assertEquals('784-1988-5310327-2', $profile->emirates_id_number); + $this->assertEquals('2028-04-11', $profile->emirates_id_expiry); + $sponsor = Sponsor::where('email', 'employer.fcm@example.com')->first(); $this->assertEquals('employer_register_fcm_token', $sponsor->fcm_token); @@ -163,15 +171,18 @@ public function test_worker_registration_setup_profile_and_login_stores_fcm_toke public function test_sponsor_registration_and_login_stores_fcm_token_and_sends_notification() { - $licenseFile = UploadedFile::fake()->create('license.pdf', 500, 'application/pdf'); - $emiratesIdFile = UploadedFile::fake()->create('emirates_id.pdf', 500, 'application/pdf'); - $response = $this->postJson('/api/sponsors/register', [ 'full_name' => 'Sponsor FCM Org', 'mobile' => '+971500000004', 'password' => 'sponsorpassword123', - 'license_file' => $licenseFile, - 'emirates_id_file' => $emiratesIdFile, + 'license' => [ + 'license_number' => '123456', + 'organization_name' => 'Sponsor FCM Co', + 'expiry_date' => '2027-01-01', + ], + 'emirates_id' => [ + 'emirates_id_number' => '784-1988-5310327-2', + ], 'organization_name' => 'Sponsor FCM Co', 'email' => 'sponsor.fcm@example.com', 'nationality' => 'Emirati', diff --git a/tests/Feature/GoogleVisionOcrApiTest.php b/tests/Feature/GoogleVisionOcrApiTest.php new file mode 100644 index 0000000..c7fb6db --- /dev/null +++ b/tests/Feature/GoogleVisionOcrApiTest.php @@ -0,0 +1,105 @@ +postJson('/api/workers/ocr/passport-vision'); + + $response->assertStatus(422) + ->assertJson([ + 'success' => false, + 'message' => 'Validation error.', + ]) + ->assertJsonValidationErrors(['passport_file']); + } + + /** + * Test successful OCR extraction using mocked Google Vision API. + */ + public function test_extract_passport_success_with_mrz() + { + // Mock Google Vision API Response + Http::fake([ + 'https://vision.googleapis.com/*' => Http::response([ + 'responses' => [ + [ + 'fullTextAnnotation' => [ + 'text' => "PASSPORT\nType: P Country: ARE\nPassport No: Z43R34255\nSurname: AL-HASHIMI\nGiven Names: AHMAD\nNationality: ARE\nDate of Birth: 03/07/1978\nSex: M\nPlace of Birth: DUBAI\nDate of Issue: 10/02/2015\nDate of Expiry: 10/02/2020\nMRZ:\nPimage('passport.jpg'); + + $response = $this->postJson('/api/workers/ocr/passport-vision', [ + 'passport_file' => $file + ]); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => [ + 'date_of_birth' => '03/07/1978', + 'date_of_expiry' => '10/02/2020', + 'date_of_issue' => '10/02/2015', + 'given_names' => 'AHMAD', + 'issuing_country' => 'ARE', + 'nationality' => 'ARE', + 'passport_number' => 'Z43R34255', + 'place_of_birth' => 'DUBAI', + ] + ]); + } + + /** + * Test OCR extraction fallback via regex when MRZ is absent. + */ + public function test_extract_passport_success_via_regex_fallback() + { + // Mock Google Vision API Response without MRZ lines + Http::fake([ + 'https://vision.googleapis.com/*' => Http::response([ + 'responses' => [ + [ + 'fullTextAnnotation' => [ + 'text' => "PASSPORT\nPassport Number: Z43R34255\nGiven Names: AHMAD\nNationality: ARE\nIssuing Country: ARE\nDate of Birth: 03/07/1978\nDate of Issue: 10/02/2015\nDate of Expiry: 10/02/2020\nPlace of Birth: DUBAI" + ] + ] + ] + ], 200) + ]); + + $file = UploadedFile::fake()->image('passport.jpg'); + + $response = $this->postJson('/api/workers/ocr/passport-vision', [ + 'passport_file' => $file + ]); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => [ + 'date_of_birth' => '03/07/1978', + 'date_of_expiry' => '10/02/2020', + 'date_of_issue' => '10/02/2015', + 'given_names' => 'AHMAD', + 'issuing_country' => 'ARE', + 'nationality' => 'ARE', + 'passport_number' => 'Z43R34255', + 'place_of_birth' => 'DUBAI', + ] + ]); + } +} diff --git a/tests/Feature/SponsorAuthApiTest.php b/tests/Feature/SponsorAuthApiTest.php index a7b9049..7dc636d 100644 --- a/tests/Feature/SponsorAuthApiTest.php +++ b/tests/Feature/SponsorAuthApiTest.php @@ -19,15 +19,26 @@ class SponsorAuthApiTest extends TestCase */ public function test_sponsor_can_register_with_license() { - $licenseFile = UploadedFile::fake()->create('license.pdf', 500, 'application/pdf'); - $emiratesIdFile = UploadedFile::fake()->create('emirates_id.pdf', 500, 'application/pdf'); - $response = $this->postJson('/api/sponsors/register', [ 'full_name' => 'Test Sponsor Organization', 'mobile' => '+971509990001', 'password' => 'securepassword', - 'license_file' => $licenseFile, - 'emirates_id_file' => $emiratesIdFile, + 'license' => [ + 'license_number' => '123456', + 'organization_name' => 'Charity Co', + 'expiry_date' => '2028-06-12', + ], + 'emirates_id' => [ + 'emirates_id_number' => '784-1988-5310327-2', + 'name' => 'Ahmad Bin Ahmed', + 'date_of_birth' => '1990-01-01', + 'nationality' => 'Emirati', + 'issue_date' => '2023-04-11', + 'expiry_date' => '2028-04-11', + 'employer' => 'Federal Authority', + 'issue_place' => 'Abu Dhabi', + 'occupation' => 'Manager' + ], 'organization_name' => 'Charity Co', 'email' => 'test.sponsor@example.com', 'nationality' => 'Emirati', @@ -51,6 +62,7 @@ public function test_sponsor_can_register_with_license() 'city', 'license_file', 'emirates_id_file', + 'emirates_id', 'license_expiry', ], 'token', @@ -61,9 +73,17 @@ public function test_sponsor_can_register_with_license() 'mobile' => '+971509990001', 'full_name' => 'Test Sponsor Organization', 'license_expiry' => '2028-06-12 00:00:00', + 'emirates_id_name' => 'Ahmad Bin Ahmed', + 'emirates_id_dob' => '1990-01-01', + 'emirates_id_issue_date' => '2023-04-11', + 'emirates_id_expiry' => '2028-04-11', + 'emirates_id_employer' => 'Federal Authority', + 'emirates_id_issue_place' => 'Abu Dhabi', + 'emirates_id_occupation' => 'Manager', ]); $sponsor = Sponsor::where('mobile', '+971509990001')->first(); + $this->assertEquals('784-1988-5310327-2', $sponsor->emirates_id); $this->assertNull($sponsor->emirates_id_file); $this->assertNull($sponsor->license_file); } @@ -500,11 +520,11 @@ public function test_sponsor_can_list_own_pending_charity_event() } /** - * Test uploading Emirates ID front/back and license files separately. + * Test registering Sponsor with explicit emirates_id, and uploading license separately. */ - public function test_sponsor_can_upload_separate_documents() + public function test_sponsor_registration_with_emirates_id_and_separate_license() { - // 1. Register Sponsor without documents + // 1. Register Sponsor with emirates_id in payload $response = $this->postJson('/api/sponsors/register', [ 'full_name' => 'Test Sponsor Sep', 'mobile' => '+971509990901', @@ -515,55 +535,19 @@ public function test_sponsor_can_upload_separate_documents() 'city' => 'Abu Dhabi', 'address' => 'Villa 45, Street 12', 'country_code' => '+971', + 'emirates_id' => [ + 'emirates_id_number' => '784-1988-5310327-2', + ], ]); $response->assertStatus(201); $sponsor = Sponsor::where('email', 'test.sponsor.sep@example.com')->first(); $this->assertNotNull($sponsor); + $this->assertEquals('784-1988-5310327-2', $sponsor->emirates_id); $this->assertNull($sponsor->license_expiry); - // 2. Upload Emirates ID Front - $frontFile = UploadedFile::fake()->create('emirates_id_front.jpg', 500, 'image/jpeg'); - $responseFront = $this->postJson('/api/sponsors/register/emirates-front', [ - 'email' => 'test.sponsor.sep@example.com', - 'emirates_id_front' => $frontFile, - ]); - - $responseFront->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'Emirates ID front uploaded and processed successfully.', - ]) - ->assertJsonStructure([ - 'ocr_extracted_data' => [ - 'emirates_id_number', - 'name', - 'nationality', - 'date_of_birth', - ] - ]); - - // 3. Upload Emirates ID Back - $backFile = UploadedFile::fake()->create('emirates_id_back.jpg', 500, 'image/jpeg'); - $responseBack = $this->postJson('/api/sponsors/register/emirates-back', [ - 'email' => 'test.sponsor.sep@example.com', - 'emirates_id_back' => $backFile, - ]); - - $responseBack->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'Emirates ID back uploaded and processed successfully.', - ]) - ->assertJsonStructure([ - 'ocr_extracted_data' => [ - 'expiry_date', - 'issue_date', - ] - ]); - - // 4. Upload License File + // 2. Upload License File $licenseFile = UploadedFile::fake()->create('license.pdf', 500, 'application/pdf'); $responseLicense = $this->postJson('/api/sponsors/register/license', [ 'email' => 'test.sponsor.sep@example.com', diff --git a/vite.config.js b/vite.config.js index 7b6b06e..f8cf04f 100644 --- a/vite.config.js +++ b/vite.config.js @@ -23,7 +23,7 @@ export default defineConfig({ port: 5173, cors: true, hmr: { - host: '192.168.0.172', + host: '192.168.0.245', }, watch: { ignored: ['**/storage/framework/views/**'],