api changes for register empyer, worker, sponsor

This commit is contained in:
mohanmd 2026-06-19 10:53:04 +05:30
parent 955fb2c67e
commit 121337eea7
18 changed files with 988 additions and 762 deletions

View File

@ -191,6 +191,8 @@ public function register(Request $request)
'email' => 'required|string|email|max:255|unique:users,email|unique:sponsors,email', 'email' => 'required|string|email|max:255|unique:users,email|unique:sponsors,email',
'phone' => 'required|string|max:50|unique:employer_profiles,phone|unique:sponsors,mobile', 'phone' => 'required|string|max:50|unique:employer_profiles,phone|unique:sponsors,mobile',
'fcm_token' => 'nullable|string|max:255', 'fcm_token' => 'nullable|string|max:255',
'emirates_id' => 'nullable|array',
'address' => 'nullable|string|max:255',
], [ ], [
'email.unique' => 'This email address is already registered.', 'email.unique' => 'This email address is already registered.',
'phone.unique' => 'This mobile number 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 'expires_at' => now()->addMinutes(10)->timestamp
], 600); ], 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 // Create inactive User
$user = User::create([ $user = User::create([
'name' => $request->name, 'name' => $request->name,
@ -235,6 +260,16 @@ public function register(Request $request)
'verification_status' => 'pending', 'verification_status' => 'pending',
'language' => 'English', 'language' => 'English',
'notifications' => true, '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 // Create unverified Sponsor
@ -253,6 +288,8 @@ public function register(Request $request)
'otp_verified_at' => null, 'otp_verified_at' => null,
'status' => 'inactive', 'status' => 'inactive',
'fcm_token' => $request->fcm_token, 'fcm_token' => $request->fcm_token,
'emirates_id' => $emiratesIdNumber,
'address' => $request->address,
]); ]);
if ($request->filled('fcm_token')) { if ($request->filled('fcm_token')) {
@ -723,144 +760,4 @@ public function resetPassword(Request $request)
], 500); ], 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);
}
}
} }

View File

@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\GoogleVisionOcrService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class GoogleVisionOcrController extends Controller
{
/**
* Upload passport image and extract fields using Google Cloud Vision API.
*/
public function extractPassport(Request $request)
{
$validator = Validator::make($request->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);
}
}

View File

@ -26,8 +26,8 @@ public function register(Request $request)
'full_name' => 'required|string|max:255', 'full_name' => 'required|string|max:255',
'mobile' => 'required|string|max:50|unique:sponsors,mobile|unique:employer_profiles,phone', 'mobile' => 'required|string|max:50|unique:sponsors,mobile|unique:employer_profiles,phone',
'password' => 'required|string|min:6', 'password' => 'required|string|min:6',
'license_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240', 'license' => 'nullable|array',
'emirates_id_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240', 'emirates_id' => 'nullable|array',
'organization_name' => 'required|string|max:255', 'organization_name' => 'required|string|max:255',
'email' => 'required|email|max:255|unique:sponsors,email|unique:users,email', 'email' => 'required|email|max:255|unique:sponsors,email|unique:users,email',
'nationality' => 'required|string|max:100', 'nationality' => 'required|string|max:100',
@ -58,22 +58,44 @@ public function register(Request $request)
} }
$licenseExpiry = $request->license_expiry; $licenseExpiry = $request->license_expiry;
if ($request->hasFile('license_file')) { $licenseInput = $request->input('license');
$licenseFile = $request->file('license_file'); if (is_array($licenseInput)) {
$ocrLicense = \App\Services\OcrDocumentService::extractSponsorLicenseData($licenseFile); $licenseExpiry = $licenseExpiry ?? ($licenseInput['expiry_date'] ?? $licenseInput['license_expiry'] ?? null);
$licenseExpiry = $licenseExpiry ?? $ocrLicense['expiry_date']; }
$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; $emiratesIdPath = null;
if ($request->hasFile('emirates_id_file')) {
$emiratesIdFile = $request->file('emirates_id_file');
\App\Services\OcrDocumentService::extractEmiratesIdFrontData($emiratesIdFile);
}
$licensePath = null; $licensePath = null;
$apiToken = Str::random(80); $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([ return Sponsor::create([
'full_name' => $request->full_name, 'full_name' => $request->full_name,
'organization_name' => $request->organization_name, 'organization_name' => $request->organization_name,
@ -86,12 +108,20 @@ public function register(Request $request)
'address' => $request->address, 'address' => $request->address,
'license_file' => $licensePath, 'license_file' => $licensePath,
'emirates_id_file' => $emiratesIdPath, 'emirates_id_file' => $emiratesIdPath,
'emirates_id' => $emiratesId,
'license_expiry' => $licenseExpiry, 'license_expiry' => $licenseExpiry,
'status' => 'active', 'status' => 'active',
'is_verified' => false, 'is_verified' => false,
'subscription_status' => 'none', 'subscription_status' => 'none',
'api_token' => $apiToken, 'api_token' => $apiToken,
'fcm_token' => $request->fcm_token, '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); ], 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. * Upload Sponsor's License document.

View File

@ -22,6 +22,13 @@ class EmployerProfile extends Model
'accommodation', 'accommodation',
'district', 'district',
'emirates_id_number', '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'
]; ];
} }

View File

@ -33,8 +33,16 @@ class Sponsor extends Authenticatable
'api_token', 'api_token',
'license_file', 'license_file',
'emirates_id_file', 'emirates_id_file',
'emirates_id',
'license_expiry', 'license_expiry',
'fcm_token', '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 = [ protected $hidden = [

View File

@ -0,0 +1,323 @@
<?php
namespace App\Services;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class GoogleVisionOcrService
{
/**
* Call Google Cloud Vision API to detect text in the uploaded image.
*/
public static function extractPassportData(UploadedFile $file): array
{
$apiKey = config('services.google.vision_api_key');
if (empty($apiKey)) {
Log::warning('Google Vision API key is not configured.');
return [
'success' => 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;
}
}

View File

@ -43,4 +43,8 @@
'sponsor_license_url' => env('SPONSOR_LICENSE_API_URL'), 'sponsor_license_url' => env('SPONSOR_LICENSE_API_URL'),
], ],
'google' => [
'vision_api_key' => env('GOOGLE_VISION_API_KEY'),
],
]; ];

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('sponsors', function (Blueprint $table) {
if (!Schema::hasColumn('sponsors', 'emirates_id')) {
$table->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');
});
}
};

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('employer_profiles', function (Blueprint $table) {
$table->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',
]);
});
}
};

View File

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('sponsors', function (Blueprint $table) {
$table->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',
]);
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('employer_profiles', function (Blueprint $table) {
$table->string('address')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('employer_profiles', function (Blueprint $table) {
$table->dropColumn('address');
});
}
};

View File

@ -28,7 +28,7 @@
"requestBody": { "requestBody": {
"required": true, "required": true,
"content": { "content": {
"multipart/form-data": { "application/json": {
"schema": { "schema": {
"type": "object", "type": "object",
"required": [ "required": [
@ -51,7 +51,7 @@
"mobile": { "mobile": {
"type": "string", "type": "string",
"example": "+971501112233", "example": "+971501112233",
"description": "Mobile phone number \u2014 must be unique. (REQUIRED)" "description": "Mobile phone number must be unique. (REQUIRED)"
}, },
"password": { "password": {
"type": "string", "type": "string",
@ -59,15 +59,65 @@
"example": "securepass123", "example": "securepass123",
"description": "Login password (min 6 characters). (REQUIRED)" "description": "Login password (min 6 characters). (REQUIRED)"
}, },
"license_file": { "license": {
"type": "string", "type": "object",
"format": "binary", "description": "Optional JSON object containing extracted organization/trade license details.",
"description": "Organization or trade license document (jpg/png/pdf, max 10MB). (REQUIRED)" "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": { "emirates_id": {
"type": "string", "type": "object",
"format": "binary", "description": "Optional JSON object containing extracted Emirates ID details.",
"description": "Emirates ID document file (jpg/png/pdf, max 10MB). (REQUIRED)" "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": { "organization_name": {
"type": "string", "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": { "/sponsors/register/license": {
"post": { "post": {
"tags": [ "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": { "/workers/config": {
"get": { "get": {
"tags": [ "tags": [
@ -2857,10 +2810,57 @@
"example": "+971509990001", "example": "+971509990001",
"description": "Sponsor's mobile phone number" "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": { "fcm_token": {
"type": "string", "type": "string",
"example": "fcm_token_example", "example": "fcm_token_example",
"description": "Firebase Cloud Messaging token for push notifications." "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": { "/employers/payment": {
"post": { "post": {
"tags": [ "tags": [

View File

@ -12,6 +12,7 @@
use App\Http\Controllers\Api\EmployerReviewController; use App\Http\Controllers\Api\EmployerReviewController;
use App\Http\Controllers\Api\SponsorAuthController; use App\Http\Controllers\Api\SponsorAuthController;
use App\Http\Controllers\Api\SponsorController; 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/setup-profile', [WorkerAuthController::class, 'setupProfile']);
Route::post('/workers/register', [WorkerAuthController::class, 'register']); Route::post('/workers/register', [WorkerAuthController::class, 'register']);
Route::post('/workers/login', [WorkerAuthController::class, 'login']); Route::post('/workers/login', [WorkerAuthController::class, 'login']);
Route::post('/workers/ocr/passport-vision', [GoogleVisionOcrController::class, 'extractPassport']);
// Unprotected Employer Mobile Auth Endpoints // Unprotected Employer Mobile Auth Endpoints
Route::post('/employers/register', [EmployerAuthController::class, 'register']); Route::post('/employers/register', [EmployerAuthController::class, 'register']);
Route::post('/employers/verify', [EmployerAuthController::class, 'verify']); 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/payment', [EmployerAuthController::class, 'payment']);
Route::post('/employers/password', [EmployerAuthController::class, 'password']); Route::post('/employers/password', [EmployerAuthController::class, 'password']);
Route::get('/employers/plans', [EmployerAuthController::class, 'plans']); Route::get('/employers/plans', [EmployerAuthController::class, 'plans']);
@ -57,8 +57,6 @@
// Sponsor Mobile Auth Endpoints (unprotected) // Sponsor Mobile Auth Endpoints (unprotected)
Route::post('/sponsors/register', [SponsorAuthController::class, 'register']); 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/register/license', [SponsorAuthController::class, 'uploadLicense']);
Route::post('/sponsors/login', [EmployerAuthController::class, 'login']); Route::post('/sponsors/login', [EmployerAuthController::class, 'login']);

View File

@ -89,9 +89,6 @@ public function test_employer_can_login()
*/ */
public function test_employer_can_register() 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 // Step 1: Register
$response = $this->postJson('/api/employers/register', [ $response = $this->postJson('/api/employers/register', [
'company_name' => 'New Company Corp', 'company_name' => 'New Company Corp',
@ -100,6 +97,18 @@ public function test_employer_can_register()
'phone' => '+971500000003', 'phone' => '+971500000003',
'password' => 'Password@123', 'password' => 'Password@123',
'password_confirmation' => '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) $response->assertStatus(201)
@ -116,105 +125,23 @@ public function test_employer_can_register()
$this->assertDatabaseHas('employer_profiles', [ $this->assertDatabaseHas('employer_profiles', [
'company_name' => 'Alice Employer Household', '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 $this->assertDatabaseHas('sponsors', [
$uploadFrontResponse = $this->postJson('/api/employers/register/emirates-front', [
'email' => 'alice@example.com', '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);
} }
/** /**

View File

@ -51,7 +51,11 @@ public function test_employer_registration_and_login_stores_fcm_token_and_sends_
'name' => 'Employer FCM Test', 'name' => 'Employer FCM Test',
'email' => 'employer.fcm@example.com', 'email' => 'employer.fcm@example.com',
'phone' => '+971500000001', '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); $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(); $user = User::where('email', 'employer.fcm@example.com')->first();
$this->assertEquals('employer_register_fcm_token', $user->fcm_token); $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(); $sponsor = Sponsor::where('email', 'employer.fcm@example.com')->first();
$this->assertEquals('employer_register_fcm_token', $sponsor->fcm_token); $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() 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', [ $response = $this->postJson('/api/sponsors/register', [
'full_name' => 'Sponsor FCM Org', 'full_name' => 'Sponsor FCM Org',
'mobile' => '+971500000004', 'mobile' => '+971500000004',
'password' => 'sponsorpassword123', 'password' => 'sponsorpassword123',
'license_file' => $licenseFile, 'license' => [
'emirates_id_file' => $emiratesIdFile, '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', 'organization_name' => 'Sponsor FCM Co',
'email' => 'sponsor.fcm@example.com', 'email' => 'sponsor.fcm@example.com',
'nationality' => 'Emirati', 'nationality' => 'Emirati',

View File

@ -0,0 +1,105 @@
<?php
namespace Tests\Feature;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class GoogleVisionOcrApiTest extends TestCase
{
/**
* Test validation requires passport_file.
*/
public function test_extract_passport_validation_fails_without_file()
{
$response = $this->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:\nP<AREAL<HASHIMI<<AHMAD<<<<<<<<<<<<<<<<<<<<<<\nZ43R342558ARE7807033M2002100<<<<<<<<<<<<<<02"
]
]
]
], 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',
]
]);
}
/**
* 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',
]
]);
}
}

View File

@ -19,15 +19,26 @@ class SponsorAuthApiTest extends TestCase
*/ */
public function test_sponsor_can_register_with_license() 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', [ $response = $this->postJson('/api/sponsors/register', [
'full_name' => 'Test Sponsor Organization', 'full_name' => 'Test Sponsor Organization',
'mobile' => '+971509990001', 'mobile' => '+971509990001',
'password' => 'securepassword', 'password' => 'securepassword',
'license_file' => $licenseFile, 'license' => [
'emirates_id_file' => $emiratesIdFile, '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', 'organization_name' => 'Charity Co',
'email' => 'test.sponsor@example.com', 'email' => 'test.sponsor@example.com',
'nationality' => 'Emirati', 'nationality' => 'Emirati',
@ -51,6 +62,7 @@ public function test_sponsor_can_register_with_license()
'city', 'city',
'license_file', 'license_file',
'emirates_id_file', 'emirates_id_file',
'emirates_id',
'license_expiry', 'license_expiry',
], ],
'token', 'token',
@ -61,9 +73,17 @@ public function test_sponsor_can_register_with_license()
'mobile' => '+971509990001', 'mobile' => '+971509990001',
'full_name' => 'Test Sponsor Organization', 'full_name' => 'Test Sponsor Organization',
'license_expiry' => '2028-06-12 00:00:00', '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(); $sponsor = Sponsor::where('mobile', '+971509990001')->first();
$this->assertEquals('784-1988-5310327-2', $sponsor->emirates_id);
$this->assertNull($sponsor->emirates_id_file); $this->assertNull($sponsor->emirates_id_file);
$this->assertNull($sponsor->license_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', [ $response = $this->postJson('/api/sponsors/register', [
'full_name' => 'Test Sponsor Sep', 'full_name' => 'Test Sponsor Sep',
'mobile' => '+971509990901', 'mobile' => '+971509990901',
@ -515,55 +535,19 @@ public function test_sponsor_can_upload_separate_documents()
'city' => 'Abu Dhabi', 'city' => 'Abu Dhabi',
'address' => 'Villa 45, Street 12', 'address' => 'Villa 45, Street 12',
'country_code' => '+971', 'country_code' => '+971',
'emirates_id' => [
'emirates_id_number' => '784-1988-5310327-2',
],
]); ]);
$response->assertStatus(201); $response->assertStatus(201);
$sponsor = Sponsor::where('email', 'test.sponsor.sep@example.com')->first(); $sponsor = Sponsor::where('email', 'test.sponsor.sep@example.com')->first();
$this->assertNotNull($sponsor); $this->assertNotNull($sponsor);
$this->assertEquals('784-1988-5310327-2', $sponsor->emirates_id);
$this->assertNull($sponsor->license_expiry); $this->assertNull($sponsor->license_expiry);
// 2. Upload Emirates ID Front // 2. Upload License File
$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
$licenseFile = UploadedFile::fake()->create('license.pdf', 500, 'application/pdf'); $licenseFile = UploadedFile::fake()->create('license.pdf', 500, 'application/pdf');
$responseLicense = $this->postJson('/api/sponsors/register/license', [ $responseLicense = $this->postJson('/api/sponsors/register/license', [
'email' => 'test.sponsor.sep@example.com', 'email' => 'test.sponsor.sep@example.com',

View File

@ -23,7 +23,7 @@ export default defineConfig({
port: 5173, port: 5173,
cors: true, cors: true,
hmr: { hmr: {
host: '192.168.0.172', host: '192.168.0.245',
}, },
watch: { watch: {
ignored: ['**/storage/framework/views/**'], ignored: ['**/storage/framework/views/**'],