OCR api changes passport, visa
This commit is contained in:
parent
40c840b90b
commit
955fb2c67e
@ -62,6 +62,10 @@ AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
OCR_API_URL=http://192.168.0.220:5000/passport
|
||||
PASSPORT_API_URL=
|
||||
VISA_API_URL=
|
||||
EMIRATES_FRONT_API_URL=
|
||||
EMIRATES_BACK_API_URL=
|
||||
SPONSOR_LICENSE_API_URL=
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
@ -13,7 +13,7 @@ class WorkerController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$workers = \App\Models\Worker::with(['skills'])
|
||||
$workers = \App\Models\Worker::with(['skills', 'documents'])
|
||||
->latest()
|
||||
->get()
|
||||
->map(function ($worker) {
|
||||
@ -56,6 +56,7 @@ public function index()
|
||||
'availability' => $worker->availability,
|
||||
'verified' => (bool)$worker->verified,
|
||||
'bio' => $worker->bio,
|
||||
'documents' => $worker->documents,
|
||||
'joined_at' => $worker->created_at ? $worker->created_at->format('Y-m-d') : 'N/A',
|
||||
];
|
||||
});
|
||||
@ -126,20 +127,16 @@ public function updateProfile(Request $request, $id)
|
||||
'phone' => 'required|string',
|
||||
'gender' => 'nullable|string',
|
||||
'language' => 'nullable|string',
|
||||
'country' => 'nullable|string',
|
||||
'city' => 'nullable|string',
|
||||
'area' => 'nullable|string',
|
||||
'preferred_location' => 'nullable|string',
|
||||
'live_in_out' => 'nullable|string',
|
||||
'experience' => 'required|string',
|
||||
'salary' => 'nullable|numeric',
|
||||
'bio' => 'nullable|string',
|
||||
'nationality' => 'nullable|string',
|
||||
'visa_status' => 'nullable|string',
|
||||
'religion' => 'nullable|string',
|
||||
'age' => 'nullable|integer',
|
||||
'in_country' => 'nullable|boolean',
|
||||
'in_country' => 'nullable',
|
||||
'preferred_job_type' => 'nullable|string',
|
||||
'skills' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$worker = \App\Models\Worker::findOrFail($id);
|
||||
@ -148,24 +145,31 @@ public function updateProfile(Request $request, $id)
|
||||
$worker->phone = $validated['phone'];
|
||||
$worker->gender = $validated['gender'] ?? $worker->gender;
|
||||
$worker->language = $validated['language'] ?? $worker->language;
|
||||
$worker->country = $validated['country'] ?? $worker->country;
|
||||
$worker->city = $validated['city'] ?? $worker->city;
|
||||
$worker->area = $validated['area'] ?? $worker->area;
|
||||
$worker->preferred_location = $validated['preferred_location'] ?? $worker->preferred_location;
|
||||
$worker->live_in_out = $validated['live_in_out'] ?? $worker->live_in_out;
|
||||
$worker->experience = $validated['experience'];
|
||||
$worker->salary = $validated['salary'] ?? $worker->salary;
|
||||
$worker->bio = $validated['bio'] ?? $worker->bio;
|
||||
$worker->nationality = $validated['nationality'] ?? $worker->nationality;
|
||||
$worker->visa_status = $validated['visa_status'] ?? $worker->visa_status;
|
||||
$worker->religion = $validated['religion'] ?? $worker->religion;
|
||||
$worker->age = $validated['age'] ?? $worker->age;
|
||||
|
||||
if (isset($validated['in_country'])) {
|
||||
$worker->in_country = (bool)$validated['in_country'];
|
||||
$worker->in_country = filter_var($validated['in_country'], FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
$worker->preferred_job_type = $validated['preferred_job_type'] ?? $worker->preferred_job_type;
|
||||
$worker->save();
|
||||
|
||||
if ($request->has('skills')) {
|
||||
$skillsArray = array_filter(array_map('trim', explode(',', $validated['skills'] ?? '')));
|
||||
$skillIds = [];
|
||||
foreach ($skillsArray as $name) {
|
||||
$skill = \App\Models\Skill::firstOrCreate(['name' => $name]);
|
||||
$skillIds[] = $skill->id;
|
||||
}
|
||||
$worker->skills()->sync($skillIds);
|
||||
}
|
||||
|
||||
return back()->with('success', "Worker #{$id} profile details moderated and updated successfully.");
|
||||
}
|
||||
|
||||
|
||||
@ -292,8 +292,8 @@ public function setupProfile(Request $request)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified registration — supports backward compatibility and multipart file registration.
|
||||
/**
|
||||
* Unified registration — supports direct JSON payloads for passport and visa.
|
||||
*/
|
||||
public function register(Request $request)
|
||||
{
|
||||
@ -314,6 +314,8 @@ public function register(Request $request)
|
||||
'live_in_out' => 'nullable|string|in:live_in,live_out',
|
||||
'preferred_location' => 'nullable|string|max:255',
|
||||
'fcm_token' => 'nullable|string|max:255',
|
||||
'passport' => 'nullable|array',
|
||||
'visa' => 'nullable|array',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@ -348,27 +350,75 @@ public function register(Request $request)
|
||||
$apiToken = Str::random(80);
|
||||
|
||||
$result = DB::transaction(function () use ($request, $email, $skillsArray, $apiToken, $inCountry) {
|
||||
// Initialize fields
|
||||
$name = $request->name;
|
||||
$nationality = $request->nationality ?? 'Not Specified';
|
||||
$age = $request->age ?? 25;
|
||||
$visaStatus = $inCountry ? $request->visa_status : null;
|
||||
|
||||
$passportDataInput = $request->input('passport');
|
||||
$visaDataInput = $request->input('visa');
|
||||
|
||||
// Process passport if provided
|
||||
if ($passportDataInput) {
|
||||
$givenNames = trim($passportDataInput['given_names'] ?? '');
|
||||
$surname = trim($passportDataInput['surname'] ?? '');
|
||||
$fullName = trim($givenNames . ' ' . $surname);
|
||||
if (empty($fullName)) {
|
||||
$fullName = $passportDataInput['name'] ?? null;
|
||||
}
|
||||
if ($fullName) {
|
||||
$name = $fullName;
|
||||
}
|
||||
|
||||
if (!empty($passportDataInput['nationality'])) {
|
||||
$codeUpper = strtoupper($passportDataInput['nationality']);
|
||||
$matchedNationality = \App\Models\Nationality::where('iso3', $codeUpper)
|
||||
->orWhere('code', $codeUpper)
|
||||
->first();
|
||||
$nationality = $matchedNationality ? $matchedNationality->name : $passportDataInput['nationality'];
|
||||
}
|
||||
|
||||
if (!empty($passportDataInput['date_of_birth'])) {
|
||||
try {
|
||||
$dob = new \DateTime($this->normaliseDateForController($passportDataInput['date_of_birth']));
|
||||
$age = (new \DateTime())->diff($dob)->y;
|
||||
} catch (\Exception $dateEx) {
|
||||
logger()->error('Passport DOB parse error in register: ' . $dateEx->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process visa if provided
|
||||
if ($visaDataInput) {
|
||||
if (!empty($visaDataInput['profession'])) {
|
||||
if (empty($visaStatus)) {
|
||||
$visaStatus = $visaDataInput['profession'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$worker = Worker::create([
|
||||
'name' => $request->name,
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'phone' => $request->phone,
|
||||
'language' => $request->language ?? 'HI',
|
||||
'password' => Hash::make($request->password),
|
||||
'nationality' => $request->nationality ?? 'Not Specified',
|
||||
'nationality' => $nationality,
|
||||
'gender' => $request->gender,
|
||||
'live_in_out' => $request->live_in_out,
|
||||
'preferred_location' => $request->preferred_location,
|
||||
'age' => $request->age ?? 25,
|
||||
'age' => $age,
|
||||
'salary' => $request->salary,
|
||||
'availability' => 'Immediate',
|
||||
'experience' => $request->experience ?? 'Not Specified',
|
||||
'religion' => 'Not Specified',
|
||||
'bio' => 'Hardworking and reliable General Helper available for immediate hire.',
|
||||
'verified' => false,
|
||||
'verified' => ($passportDataInput || $visaDataInput) ? true : false,
|
||||
'status' => 'active',
|
||||
'api_token' => $apiToken,
|
||||
'in_country' => $inCountry,
|
||||
'visa_status' => $inCountry ? $request->visa_status : null,
|
||||
'visa_status' => $visaStatus,
|
||||
'preferred_job_type' => $request->preferred_job_type,
|
||||
'fcm_token' => $request->fcm_token,
|
||||
]);
|
||||
@ -380,6 +430,46 @@ public function register(Request $request)
|
||||
}
|
||||
}
|
||||
|
||||
// Create passport document if provided
|
||||
if ($passportDataInput) {
|
||||
$passportNum = $passportDataInput['passport_number'] ?? ('P' . rand(1000000, 9999999));
|
||||
$expiryDate = $passportDataInput['date_of_expiry'] ?? null;
|
||||
$issueDate = $passportDataInput['date_of_issue'] ?? null;
|
||||
|
||||
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(8)->toDateString();
|
||||
$issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : now()->subYears(3)->toDateString();
|
||||
|
||||
$worker->documents()->create([
|
||||
'type' => 'passport',
|
||||
'number' => $passportNum,
|
||||
'issue_date' => $issueDateFormatted,
|
||||
'expiry_date' => $expiryDateFormatted,
|
||||
'ocr_accuracy' => 99.0,
|
||||
'file_path' => null,
|
||||
'ocr_data' => $passportDataInput,
|
||||
]);
|
||||
}
|
||||
|
||||
// Create visa document if provided
|
||||
if ($visaDataInput) {
|
||||
$visaNum = $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999));
|
||||
$expiryDate = $visaDataInput['expiry_date'] ?? null;
|
||||
$issueDate = $visaDataInput['issue_date'] ?? null;
|
||||
|
||||
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(2)->toDateString();
|
||||
$issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : now()->subYears(2)->toDateString();
|
||||
|
||||
$worker->documents()->create([
|
||||
'type' => 'visa',
|
||||
'number' => $visaNum,
|
||||
'issue_date' => $issueDateFormatted,
|
||||
'expiry_date' => $expiryDateFormatted,
|
||||
'ocr_accuracy' => 99.0,
|
||||
'file_path' => null,
|
||||
'ocr_data' => $visaDataInput,
|
||||
]);
|
||||
}
|
||||
|
||||
return $worker;
|
||||
});
|
||||
|
||||
@ -387,7 +477,7 @@ public function register(Request $request)
|
||||
\App\Services\FCMService::sendPushNotification(
|
||||
$request->fcm_token,
|
||||
'Welcome to Migrant',
|
||||
'Worker registered successfully. Please upload your passport and visa documents.'
|
||||
'Worker registered successfully.'
|
||||
);
|
||||
}
|
||||
|
||||
@ -395,7 +485,7 @@ public function register(Request $request)
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Worker registered successfully. Please upload your passport and visa documents.',
|
||||
'message' => 'Worker registered successfully.',
|
||||
'data' => ['worker' => $result, 'token' => $apiToken]
|
||||
], 201);
|
||||
|
||||
@ -403,223 +493,15 @@ public function register(Request $request)
|
||||
logger()->error('Worker Registration Failure: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred during worker registration. Please try again.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Document Upload Endpoints (protected — require Bearer token)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Upload & OCR-process the worker's passport document.
|
||||
* POST /workers/register/passport (auth.worker)
|
||||
*
|
||||
* Calls the dedicated Passport OCR API (http://192.168.0.252:5000/passport).
|
||||
* Creates or updates the worker's passport document record.
|
||||
* Optionally syncs name, nationality, and age extracted from OCR onto the worker profile.
|
||||
*/
|
||||
public function registerPassport(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'passport_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
||||
'sync_profile' => 'nullable|in:true,false,1,0,TRUE,FALSE',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
/** @var \App\Models\Worker $worker */
|
||||
$worker = $request->attributes->get('worker');
|
||||
|
||||
try {
|
||||
$file = $request->file('passport_file');
|
||||
$ocrRes = \App\Services\OcrDocumentService::extractPassportData($file);
|
||||
|
||||
if (empty($ocrRes['success'])) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Passport OCR extraction failed. Please upload a clear image and try again.',
|
||||
'errors' => [
|
||||
'passport_file' => ['Passport OCR API could not read the document. Please try again.']
|
||||
]
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Build document data from OCR
|
||||
$number = $ocrRes['document_number'] ?? ('P' . rand(1000000, 9999999));
|
||||
$expiryDate = $ocrRes['expiry_date'] ?? now()->addYears(8)->toDateString();
|
||||
$issueDate = !empty($ocrRes['expiry_date'])
|
||||
? date('Y-m-d', strtotime($ocrRes['expiry_date'] . ' -5 years'))
|
||||
: now()->subYears(3)->toDateString();
|
||||
|
||||
// Upsert passport document (update if already exists, create if not)
|
||||
$worker->documents()->updateOrCreate(
|
||||
['type' => 'passport'],
|
||||
[
|
||||
'number' => $number,
|
||||
'issue_date' => $issueDate,
|
||||
'expiry_date' => $expiryDate,
|
||||
'ocr_accuracy' => 98.5,
|
||||
'file_path' => null,
|
||||
]
|
||||
);
|
||||
|
||||
// Optionally sync OCR-extracted profile fields
|
||||
$profileUpdates = ['verified' => true];
|
||||
$syncProfile = filter_var($request->input('sync_profile', true), FILTER_VALIDATE_BOOLEAN);
|
||||
if ($syncProfile) {
|
||||
if (!empty($ocrRes['name'])) {
|
||||
$profileUpdates['name'] = $ocrRes['name'];
|
||||
}
|
||||
|
||||
if (!empty($ocrRes['nationality'])) {
|
||||
$codeUpper = strtoupper($ocrRes['nationality']);
|
||||
$matchedNationality = \App\Models\Nationality::where('iso3', $codeUpper)
|
||||
->orWhere('code', $codeUpper)
|
||||
->first();
|
||||
$profileUpdates['nationality'] = $matchedNationality ? $matchedNationality->name : $ocrRes['nationality'];
|
||||
}
|
||||
|
||||
if (!empty($ocrRes['date_of_birth'])) {
|
||||
try {
|
||||
$dob = new \DateTime($ocrRes['date_of_birth']);
|
||||
$profileUpdates['age'] = (new \DateTime())->diff($dob)->y;
|
||||
} catch (\Exception $dateEx) {
|
||||
logger()->error('Passport DOB parse error: ' . $dateEx->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($profileUpdates)) {
|
||||
$worker->update($profileUpdates);
|
||||
}
|
||||
|
||||
$worker->load(['skills', 'documents']);
|
||||
|
||||
$passport = $worker->documents->where('type', 'passport')->first();
|
||||
$passportData = null;
|
||||
if ($passport) {
|
||||
$passportData = array_merge($passport->toArray(), [
|
||||
'document_number' => $ocrRes['document_number'],
|
||||
'expiry_date' => $ocrRes['expiry_date'],
|
||||
'date_of_birth' => $ocrRes['date_of_birth'] ?? null,
|
||||
'nationality' => $ocrRes['nationality'] ?? null,
|
||||
'name' => $ocrRes['name'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Passport uploaded and processed successfully.',
|
||||
'data' => [
|
||||
'worker' => $worker,
|
||||
'passport' => $passportData,
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Worker Passport Upload Failure: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while processing the passport. Please try again.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload & OCR-process the worker's visa document.
|
||||
* POST /workers/register/visa (auth.worker)
|
||||
*
|
||||
* Calls the dedicated Visa OCR API (http://192.168.0.252:5000/visa).
|
||||
* Creates or updates the worker's visa document record.
|
||||
*/
|
||||
public function registerVisa(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'visa_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
/** @var \App\Models\Worker $worker */
|
||||
$worker = $request->attributes->get('worker');
|
||||
|
||||
try {
|
||||
$file = $request->file('visa_file');
|
||||
$ocrRes = \App\Services\OcrDocumentService::extractVisaData($file);
|
||||
|
||||
$number = $ocrRes['document_number'] ?? ('V' . rand(1000000, 9999999));
|
||||
$expiryDate = $ocrRes['expiry_date'] ?? now()->addYears(2)->toDateString();
|
||||
$issueDate = $ocrRes['issue_date'] ?? date('Y-m-d', strtotime($expiryDate . ' -2 years'));
|
||||
$accuracy = $ocrRes['ocr_accuracy'] ?? ($ocrRes['success'] ? 98.5 : 0.0);
|
||||
|
||||
// Upsert visa document
|
||||
$worker->documents()->updateOrCreate(
|
||||
['type' => 'visa'],
|
||||
[
|
||||
'number' => $number,
|
||||
'issue_date' => $issueDate,
|
||||
'expiry_date' => $expiryDate,
|
||||
'ocr_accuracy' => $accuracy,
|
||||
'file_path' => null,
|
||||
]
|
||||
);
|
||||
|
||||
$worker->update(['verified' => true]);
|
||||
|
||||
$worker->load(['skills', 'documents']);
|
||||
|
||||
$visa = $worker->documents->where('type', 'visa')->first();
|
||||
$visaData = null;
|
||||
if ($visa) {
|
||||
$visaData = array_merge($visa->toArray(), [
|
||||
'document_number' => $ocrRes['document_number'] ?? null,
|
||||
'expiry_date' => $ocrRes['expiry_date'] ?? null,
|
||||
'issue_date' => $ocrRes['issue_date'] ?? null,
|
||||
'visa_type' => $ocrRes['visa_type'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Visa uploaded and processed successfully.',
|
||||
'data' => [
|
||||
'worker' => $worker,
|
||||
'visa' => $visaData,
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Worker Visa Upload Failure: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while processing the visa. Please try again.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
'message' => 'Registration failed.',
|
||||
'error' => app()->environment('local', 'testing') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate a worker and return a secure Bearer token.
|
||||
*/
|
||||
public function login(Request $request)
|
||||
*/public function login(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'phone' => 'required_without:email|nullable|string',
|
||||
@ -1057,4 +939,35 @@ public function languages(Request $request)
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* E.g. "14/05/1990" -> "1990-05-14"
|
||||
*/
|
||||
private function normaliseDateForController(?string $raw): ?string
|
||||
{
|
||||
if (empty($raw)) {
|
||||
return null;
|
||||
}
|
||||
$raw = trim($raw);
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw)) {
|
||||
return $raw;
|
||||
}
|
||||
try {
|
||||
if (preg_match('/^(\d{1,2})[\/\- ]([A-Za-z0-9]+)[\/\- ](\d{4})$/', $raw, $m)) {
|
||||
$day = str_pad($m[1], 2, '0', STR_PAD_LEFT);
|
||||
$month = $m[2];
|
||||
$year = $m[3];
|
||||
if (is_numeric($month)) {
|
||||
$month = str_pad($month, 2, '0', STR_PAD_LEFT);
|
||||
return "{$year}-{$month}-{$day}";
|
||||
}
|
||||
$ts = strtotime("{$day} {$month} {$year}");
|
||||
return $ts ? date('Y-m-d', $ts) : $raw;
|
||||
}
|
||||
$dt = new \DateTime($raw);
|
||||
return $dt->format('Y-m-d');
|
||||
} catch (\Exception $e) {
|
||||
return $raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,11 @@ class WorkerDocument extends Model
|
||||
'expiry_date',
|
||||
'ocr_accuracy',
|
||||
'file_path',
|
||||
'ocr_data',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'ocr_data' => 'array',
|
||||
];
|
||||
|
||||
public function worker()
|
||||
|
||||
@ -21,7 +21,7 @@ public static function extractData(UploadedFile $file): array
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Dedicated Passport OCR → http://192.168.0.252:5000/passport
|
||||
// Dedicated Passport OCR API
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@ -52,7 +52,10 @@ public static function extractPassportData(UploadedFile $file): array
|
||||
];
|
||||
|
||||
try {
|
||||
$apiUrl = config('services.ocr.passport_url', 'http://192.168.0.252:5000/passport');
|
||||
$apiUrl = config('services.ocr.passport_url');
|
||||
if (empty($apiUrl)) {
|
||||
throw new \Exception('Passport OCR API URL is not configured.');
|
||||
}
|
||||
$contents = $file->getContent();
|
||||
if ($contents === '' || $contents === false) {
|
||||
$contents = ' ';
|
||||
@ -191,7 +194,7 @@ private static function normaliseDate(string $raw): string
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Dedicated Visa OCR → http://192.168.0.252:5000/visa
|
||||
// Dedicated Visa OCR API
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@ -220,7 +223,10 @@ public static function extractVisaData(UploadedFile $file): array
|
||||
];
|
||||
|
||||
try {
|
||||
$apiUrl = config('services.ocr.visa_url', 'http://192.168.0.252:5000/visa');
|
||||
$apiUrl = config('services.ocr.visa_url');
|
||||
if (empty($apiUrl)) {
|
||||
throw new \Exception('Visa OCR API URL is not configured.');
|
||||
}
|
||||
$contents = $file->getContent();
|
||||
if ($contents === '' || $contents === false) {
|
||||
$contents = ' ';
|
||||
@ -293,7 +299,7 @@ public static function extractVisaData(UploadedFile $file): array
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Dedicated Emirates ID Front OCR → http://192.168.0.231:5000/emirates_front
|
||||
// Dedicated Emirates ID Front OCR API
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@ -315,7 +321,10 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
|
||||
];
|
||||
|
||||
try {
|
||||
$apiUrl = config('services.ocr.emirates_front_url', 'http://192.168.0.231:5000/emirates_front');
|
||||
$apiUrl = config('services.ocr.emirates_front_url');
|
||||
if (empty($apiUrl)) {
|
||||
throw new \Exception('Emirates ID Front OCR API URL is not configured.');
|
||||
}
|
||||
$contents = $file->getContent();
|
||||
if ($contents === '' || $contents === false) {
|
||||
$contents = ' ';
|
||||
@ -396,7 +405,7 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Dedicated Emirates ID Back OCR → http://192.168.0.231:5000/emirates_back
|
||||
// Dedicated Emirates ID Back OCR API
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@ -415,7 +424,10 @@ public static function extractEmiratesIdBackData(UploadedFile $file): array
|
||||
];
|
||||
|
||||
try {
|
||||
$apiUrl = config('services.ocr.emirates_back_url', 'http://192.168.0.231:5000/emirates_back');
|
||||
$apiUrl = config('services.ocr.emirates_back_url');
|
||||
if (empty($apiUrl)) {
|
||||
throw new \Exception('Emirates ID Back OCR API URL is not configured.');
|
||||
}
|
||||
$contents = $file->getContent();
|
||||
if ($contents === '' || $contents === false) {
|
||||
$contents = ' ';
|
||||
@ -525,7 +537,7 @@ private static function emiratesIdBackTestFallback(): array
|
||||
|
||||
/**
|
||||
* Extract data from a Sponsor License document.
|
||||
* Calls http://192.168.0.252:5000/sponsor_license
|
||||
* Calls the Sponsor License OCR API
|
||||
*
|
||||
* @param UploadedFile $file
|
||||
* @return array{success: bool, expiry_date: string|null, license_number: string|null, organization_name: string|null}
|
||||
@ -540,7 +552,10 @@ public static function extractSponsorLicenseData(UploadedFile $file): array
|
||||
];
|
||||
|
||||
try {
|
||||
$apiUrl = config('services.ocr.sponsor_license_url', 'http://192.168.0.252:5000/sponsor_license');
|
||||
$apiUrl = config('services.ocr.sponsor_license_url');
|
||||
if (empty($apiUrl)) {
|
||||
throw new \Exception('Sponsor License OCR API URL is not configured.');
|
||||
}
|
||||
if (app()->environment('testing')) {
|
||||
return self::sponsorLicenseTestFallback();
|
||||
}
|
||||
|
||||
@ -36,11 +36,11 @@
|
||||
],
|
||||
|
||||
'ocr' => [
|
||||
'passport_url' => env('PASSPORT_API_URL', 'http://192.168.0.252:5000/passport'),
|
||||
'visa_url' => env('VISA_API_URL', 'http://192.168.0.252:5000/visa'),
|
||||
'emirates_front_url' => env('EMIRATES_FRONT_API_URL', 'http://192.168.0.231:5000/emirates_front'),
|
||||
'emirates_back_url' => env('EMIRATES_BACK_API_URL', 'http://192.168.0.231:5000/emirates_back'),
|
||||
'sponsor_license_url' => env('SPONSOR_LICENSE_API_URL', 'http://192.168.0.252:5000/sponsor_license'),
|
||||
'passport_url' => env('PASSPORT_API_URL'),
|
||||
'visa_url' => env('VISA_API_URL'),
|
||||
'emirates_front_url' => env('EMIRATES_FRONT_API_URL'),
|
||||
'emirates_back_url' => env('EMIRATES_BACK_API_URL'),
|
||||
'sponsor_license_url' => env('SPONSOR_LICENSE_API_URL'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@ -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('worker_documents', function (Blueprint $table) {
|
||||
$table->json('ocr_data')->nullable()->after('file_path');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('worker_documents', function (Blueprint $table) {
|
||||
$table->dropColumn('ocr_data');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -19,9 +19,11 @@
|
||||
"paths": {
|
||||
"/sponsors/register": {
|
||||
"post": {
|
||||
"tags": ["Sponsor/Auth"],
|
||||
"tags": [
|
||||
"Sponsor/Auth"
|
||||
],
|
||||
"summary": "Register Sponsor Account",
|
||||
"description": "Registers a new sponsor with basic information and uploads their organization/trade license. Sponsors have restricted access — dashboard and charity events only. No payment required.",
|
||||
"description": "Registers a new sponsor with basic information and uploads their organization/trade license. Sponsors have restricted access \u2014 dashboard and charity events only. No payment required.",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@ -49,7 +51,7 @@
|
||||
"mobile": {
|
||||
"type": "string",
|
||||
"example": "+971501112233",
|
||||
"description": "Mobile phone number — must be unique. (REQUIRED)"
|
||||
"description": "Mobile phone number \u2014 must be unique. (REQUIRED)"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
@ -122,13 +124,24 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean", "example": true},
|
||||
"message": {"type": "string", "example": "Sponsor account registered successfully. Your license is pending admin review."},
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Sponsor account registered successfully. Your license is pending admin review."
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sponsor": {"$ref": "#/components/schemas/Sponsor"},
|
||||
"token": {"type": "string", "example": "abc123...bearerToken..."}
|
||||
"sponsor": {
|
||||
"$ref": "#/components/schemas/Sponsor"
|
||||
},
|
||||
"token": {
|
||||
"type": "string",
|
||||
"example": "abc123...bearerToken..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -136,7 +149,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {"description": "Validation error (duplicate mobile/email, missing required fields)."}
|
||||
"422": {
|
||||
"description": "Validation error (duplicate mobile/email, missing required fields)."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -183,27 +198,58 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean", "example": true},
|
||||
"message": {"type": "string", "example": "Emirates ID front uploaded and processed successfully."},
|
||||
"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}
|
||||
"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"}
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "info@alrashidi.ae"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {"description": "Sponsor account not found."},
|
||||
"422": {"description": "Validation error."}
|
||||
"404": {
|
||||
"description": "Sponsor account not found."
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -250,25 +296,50 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean", "example": true},
|
||||
"message": {"type": "string", "example": "Emirates ID back uploaded and processed successfully."},
|
||||
"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}
|
||||
"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"}
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "info@alrashidi.ae"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {"description": "Sponsor account not found."},
|
||||
"422": {"description": "Validation error."}
|
||||
"404": {
|
||||
"description": "Sponsor account not found."
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -315,25 +386,50 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean", "example": true},
|
||||
"message": {"type": "string", "example": "License uploaded and processed successfully."},
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "License uploaded and processed successfully."
|
||||
},
|
||||
"ocr_extracted_data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean", "example": true},
|
||||
"expiry_date": {"type": "string", "example": "2028-06-12"},
|
||||
"license_number": {"type": "string", "example": "123456"},
|
||||
"organization_name": {"type": "string", "example": "Charity Co"}
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"expiry_date": {
|
||||
"type": "string",
|
||||
"example": "2028-06-12"
|
||||
},
|
||||
"license_number": {
|
||||
"type": "string",
|
||||
"example": "123456"
|
||||
},
|
||||
"organization_name": {
|
||||
"type": "string",
|
||||
"example": "Charity Co"
|
||||
}
|
||||
}
|
||||
},
|
||||
"email": {"type": "string", "example": "info@alrashidi.ae"}
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "info@alrashidi.ae"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {"description": "Sponsor account not found."},
|
||||
"422": {"description": "Validation error."}
|
||||
"404": {
|
||||
"description": "Sponsor account not found."
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -398,7 +494,10 @@
|
||||
"role": {
|
||||
"type": "string",
|
||||
"example": "sponsor",
|
||||
"enum": ["employer", "sponsor"]
|
||||
"enum": [
|
||||
"employer",
|
||||
"sponsor"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
@ -419,7 +518,9 @@
|
||||
},
|
||||
"/sponsors/dashboard": {
|
||||
"get": {
|
||||
"tags": ["Sponsor"],
|
||||
"tags": [
|
||||
"Sponsor"
|
||||
],
|
||||
"summary": "Sponsor Dashboard",
|
||||
"description": "Returns the sponsor profile summary, latest charity events, and total & active counts for employers and workers.",
|
||||
"responses": {
|
||||
@ -481,23 +582,52 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {"description": "Unauthenticated."}
|
||||
"401": {
|
||||
"description": "Unauthenticated."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sponsors/charity-events": {
|
||||
"get": {
|
||||
"tags": ["Sponsor"],
|
||||
"tags": [
|
||||
"Sponsor"
|
||||
],
|
||||
"summary": "List Charity Events",
|
||||
"description": "Returns a paginated list of all charity and announcement events. Optionally filter by type.",
|
||||
"parameters": [
|
||||
{"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}},
|
||||
{"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 15}},
|
||||
{"name": "type", "in": "query", "schema": {"type": "string"}, "description": "Optional event type filter (e.g. 'charity', 'update')."}
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 15
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional event type filter (e.g. 'charity', 'update')."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {"description": "Events retrieved successfully."},
|
||||
"401": {"description": "Unauthenticated."}
|
||||
"200": {
|
||||
"description": "Events retrieved successfully."
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthenticated."
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
@ -587,12 +717,18 @@
|
||||
},
|
||||
"/sponsors/profile": {
|
||||
"get": {
|
||||
"tags": ["Sponsor"],
|
||||
"tags": [
|
||||
"Sponsor"
|
||||
],
|
||||
"summary": "Get Sponsor Profile",
|
||||
"description": "Returns the full profile of the authenticated sponsor.",
|
||||
"responses": {
|
||||
"200": {"description": "Profile retrieved successfully."},
|
||||
"401": {"description": "Unauthenticated."}
|
||||
"200": {
|
||||
"description": "Profile retrieved successfully."
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthenticated."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -602,7 +738,7 @@
|
||||
"Worker/Auth"
|
||||
],
|
||||
"summary": "Register Worker Account (Step 1)",
|
||||
"description": "Creates the worker account and returns a secure Bearer token.\n\nDocument upload is a **separate step** after registration:\n- `POST /workers/register/passport` — upload passport (OCR via `http://192.168.0.252:5000/passport`)\n- `POST /workers/register/visa` — upload visa (OCR via `http://192.168.0.252:5000/visa`)\n\nBoth document endpoints require the Bearer token returned here.",
|
||||
"description": "Creates the worker account and returns a secure Bearer token.\n\nDocument upload is a **separate step** after registration:\n- `POST /workers/register/passport` \u2014 upload passport (OCR-processed)\n- `POST /workers/register/visa` \u2014 upload visa (OCR-processed)\n\nBoth document endpoints require the Bearer token returned here.",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@ -687,13 +823,20 @@
|
||||
},
|
||||
"live_in_out": {
|
||||
"type": "string",
|
||||
"enum": ["live_in", "live_out"],
|
||||
"enum": [
|
||||
"live_in",
|
||||
"live_out"
|
||||
],
|
||||
"example": "live_out",
|
||||
"description": "Accommodation preference (live_in, live_out)."
|
||||
},
|
||||
"gender": {
|
||||
"type": "string",
|
||||
"enum": ["male", "female", "other"],
|
||||
"enum": [
|
||||
"male",
|
||||
"female",
|
||||
"other"
|
||||
],
|
||||
"example": "male",
|
||||
"description": "Gender of the worker (male, female, other)."
|
||||
},
|
||||
@ -706,6 +849,92 @@
|
||||
"type": "string",
|
||||
"example": "fcm_token_example",
|
||||
"description": "Firebase Cloud Messaging token for push notifications."
|
||||
},
|
||||
"passport": {
|
||||
"type": "object",
|
||||
"description": "Optional JSON object containing extracted passport details.",
|
||||
"properties": {
|
||||
"authority": {
|
||||
"type": "string"
|
||||
},
|
||||
"date_of_birth": {
|
||||
"type": "string",
|
||||
"example": "14/05/1990"
|
||||
},
|
||||
"date_of_expiry": {
|
||||
"type": "string",
|
||||
"example": "05/07/2022"
|
||||
},
|
||||
"date_of_issue": {
|
||||
"type": "string",
|
||||
"example": "05/07/2017"
|
||||
},
|
||||
"document_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"given_names": {
|
||||
"type": "string"
|
||||
},
|
||||
"issuing_country": {
|
||||
"type": "string"
|
||||
},
|
||||
"nationality": {
|
||||
"type": "string"
|
||||
},
|
||||
"passport_number": {
|
||||
"type": "string"
|
||||
},
|
||||
"personal_number": {
|
||||
"type": "string"
|
||||
},
|
||||
"place_of_birth": {
|
||||
"type": "string"
|
||||
},
|
||||
"sex": {
|
||||
"type": "string"
|
||||
},
|
||||
"surname": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"visa": {
|
||||
"type": "object",
|
||||
"description": "Optional JSON object containing extracted visa details.",
|
||||
"properties": {
|
||||
"accompanied_by": {
|
||||
"type": "string"
|
||||
},
|
||||
"expiry_date": {
|
||||
"type": "string",
|
||||
"example": "2024-02-15"
|
||||
},
|
||||
"file_number": {
|
||||
"type": "string"
|
||||
},
|
||||
"id_number": {
|
||||
"type": "string"
|
||||
},
|
||||
"issue_date": {
|
||||
"type": "string",
|
||||
"example": "2022-02-16"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"passport_number": {
|
||||
"type": "string"
|
||||
},
|
||||
"place_of_issue": {
|
||||
"type": "string"
|
||||
},
|
||||
"profession": {
|
||||
"type": "string"
|
||||
},
|
||||
"sponsor": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -746,7 +975,7 @@
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error — field validation failed or Passport OCR extraction failed.",
|
||||
"description": "Validation error \u2014 field validation failed or Passport OCR extraction failed.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@ -765,13 +994,21 @@
|
||||
"properties": {
|
||||
"phone": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"example": ["This mobile number is already registered."]
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"This mobile number is already registered."
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"example": ["The name field is required."]
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"The name field is required."
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -787,9 +1024,18 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean", "example": false},
|
||||
"message": {"type": "string", "example": "An error occurred during worker registration. Please try again."},
|
||||
"error": {"type": "string", "example": "Internal Server Error"}
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "An error occurred during worker registration. Please try again."
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"example": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -798,179 +1044,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workers/register/passport": {
|
||||
"post": {
|
||||
"tags": ["Worker/Auth"],
|
||||
"summary": "Upload Worker Passport (Step 2)",
|
||||
"description": "**Authenticated — requires Bearer token from `POST /workers/register`.**\n\nUploads the worker's passport and forwards it to the Passport OCR API (`http://192.168.0.252:5000/passport`) to extract: passport number, expiry date, date of birth (→ age), nationality (ISO-3 → demonym), and full name.\n\nThe extracted data is saved as a `passport` document record (`updateOrCreate`). When `sync_profile` is `true` (default), the worker's `name`, `nationality`, and `age` are also updated from OCR values.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["passport_file"],
|
||||
"properties": {
|
||||
"passport_file": {
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
"description": "Passport scan or photo (jpg/png/pdf, max 10MB). Forwarded to http://192.168.0.252:5000/passport for OCR. (REQUIRED)"
|
||||
},
|
||||
"sync_profile": {
|
||||
"type": "boolean",
|
||||
"example": true,
|
||||
"description": "If true (default), updates worker name, nationality, and age from OCR results."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Passport uploaded and OCR-processed successfully.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean", "example": true},
|
||||
"message": {"type": "string", "example": "Passport uploaded and processed successfully."},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"worker": {"$ref": "#/components/schemas/Worker"},
|
||||
"passport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"type": "string", "example": "passport"},
|
||||
"number": {"type": "string", "example": "A12345678"},
|
||||
"issue_date": {"type": "string", "format": "date", "example": "2018-06-01"},
|
||||
"expiry_date": {"type": "string", "format": "date", "example": "2028-06-01"},
|
||||
"ocr_accuracy": {"type": "number", "example": 98.5},
|
||||
"document_number": {"type": "string", "example": "A12345678"},
|
||||
"date_of_birth": {"type": "string", "example": "1995-03-14"},
|
||||
"nationality": {"type": "string", "example": "IND"},
|
||||
"name": {"type": "string", "example": "AMINA AL-MASRY"}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error or Passport OCR API failure.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean", "example": false},
|
||||
"message": {"type": "string", "example": "Passport OCR extraction failed. Please upload a clear image and try again."},
|
||||
"errors": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"passport_file": {"type": "array", "items": {"type": "string"}, "example": ["Passport OCR API could not read the document. Please try again."]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {"description": "Unauthenticated — missing or invalid Bearer token."},
|
||||
"500": {"description": "Server error during passport processing."}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workers/register/visa": {
|
||||
"post": {
|
||||
"tags": ["Worker/Auth"],
|
||||
"summary": "Upload Worker Visa (Step 3)",
|
||||
"description": "**Authenticated — requires Bearer token from `POST /workers/register`.**\n\nUploads the worker's visa and forwards it to the Visa OCR API (`http://192.168.0.252:5000/visa`) to extract: visa number, issue date, expiry date, and visa type.\n\nThe extracted data is saved as a `visa` document record (`updateOrCreate` — safe to call multiple times).",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["visa_file"],
|
||||
"properties": {
|
||||
"visa_file": {
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
"description": "Visa scan or photo (jpg/png/pdf, max 10MB). Forwarded to http://192.168.0.252:5000/visa for OCR. (REQUIRED)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Visa uploaded and OCR-processed successfully.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean", "example": true},
|
||||
"message": {"type": "string", "example": "Visa uploaded and processed successfully."},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"worker": {"$ref": "#/components/schemas/Worker"},
|
||||
"visa": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"type": "string", "example": "visa"},
|
||||
"number": {"type": "string", "example": "V98765432"},
|
||||
"issue_date": {"type": "string", "format": "date", "example": "2023-01-15"},
|
||||
"expiry_date": {"type": "string", "format": "date", "example": "2025-01-14"},
|
||||
"ocr_accuracy": {"type": "number", "example": 98.5},
|
||||
"document_number": {"type": "string", "example": "V98765432"},
|
||||
"visa_type": {"type": "string", "example": "Residence"}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean", "example": false},
|
||||
"message": {"type": "string", "example": "Validation error."},
|
||||
"errors": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"visa_file": {"type": "array", "items": {"type": "string"}, "example": ["The visa file field is required."]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {"description": "Unauthenticated — missing or invalid Bearer token."},
|
||||
"500": {"description": "Server error during visa processing."}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workers/login": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -1098,7 +1171,7 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Hindi (हिन्दी)"
|
||||
"example": "Hindi (\u0939\u093f\u0928\u094d\u0926\u0940)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1941,13 +2014,20 @@
|
||||
},
|
||||
"gender": {
|
||||
"type": "string",
|
||||
"enum": ["male", "female", "other"],
|
||||
"enum": [
|
||||
"male",
|
||||
"female",
|
||||
"other"
|
||||
],
|
||||
"example": "male",
|
||||
"description": "Gender of the worker (male, female, other)."
|
||||
},
|
||||
"live_in_out": {
|
||||
"type": "string",
|
||||
"enum": ["live_in", "live_out"],
|
||||
"enum": [
|
||||
"live_in",
|
||||
"live_out"
|
||||
],
|
||||
"example": "live_out",
|
||||
"description": "Accommodation preference (live_in, live_out)."
|
||||
},
|
||||
@ -2083,7 +2163,6 @@
|
||||
"type": "string",
|
||||
"example": "Highly certified housekeeper and babysitter with cooking experience."
|
||||
},
|
||||
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@ -2840,7 +2919,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"/employers/register/emirates-front": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -3159,7 +3237,10 @@
|
||||
"role": {
|
||||
"type": "string",
|
||||
"example": "employer",
|
||||
"enum": ["employer", "sponsor"]
|
||||
"enum": [
|
||||
"employer",
|
||||
"sponsor"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
@ -5021,21 +5102,66 @@
|
||||
"Sponsor": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "integer", "example": 1},
|
||||
"full_name": {"type": "string", "example": "Mohammed Al-Rashidi"},
|
||||
"organization_name": {"type": "string", "example": "Al-Rashidi Charitable Foundation"},
|
||||
"email": {"type": "string", "example": "sponsor.971501112233@migrant.ae"},
|
||||
"mobile": {"type": "string", "example": "+971501112233"},
|
||||
"nationality": {"type": "string", "example": "UAE"},
|
||||
"city": {"type": "string", "example": "Dubai"},
|
||||
"address": {"type": "string", "example": "Villa 12, Jumeirah 2"},
|
||||
"country_code": {"type": "string", "example": "+971"},
|
||||
"is_verified": {"type": "boolean", "example": false},
|
||||
"status": {"type": "string", "example": "active"},
|
||||
"license_file": {"type": "string", "example": "uploads/licenses/1234567890_license_trade.pdf"},
|
||||
"emirates_id_file": {"type": "string", "example": "uploads/licenses/1234567890_emirates_id.pdf"},
|
||||
"fcm_token": {"type": "string", "example": "fcm_token_example"},
|
||||
"created_at": {"type": "string", "format": "date-time"}
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"example": 1
|
||||
},
|
||||
"full_name": {
|
||||
"type": "string",
|
||||
"example": "Mohammed Al-Rashidi"
|
||||
},
|
||||
"organization_name": {
|
||||
"type": "string",
|
||||
"example": "Al-Rashidi Charitable Foundation"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "sponsor.971501112233@migrant.ae"
|
||||
},
|
||||
"mobile": {
|
||||
"type": "string",
|
||||
"example": "+971501112233"
|
||||
},
|
||||
"nationality": {
|
||||
"type": "string",
|
||||
"example": "UAE"
|
||||
},
|
||||
"city": {
|
||||
"type": "string",
|
||||
"example": "Dubai"
|
||||
},
|
||||
"address": {
|
||||
"type": "string",
|
||||
"example": "Villa 12, Jumeirah 2"
|
||||
},
|
||||
"country_code": {
|
||||
"type": "string",
|
||||
"example": "+971"
|
||||
},
|
||||
"is_verified": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "active"
|
||||
},
|
||||
"license_file": {
|
||||
"type": "string",
|
||||
"example": "uploads/licenses/1234567890_license_trade.pdf"
|
||||
},
|
||||
"emirates_id_file": {
|
||||
"type": "string",
|
||||
"example": "uploads/licenses/1234567890_emirates_id.pdf"
|
||||
},
|
||||
"fcm_token": {
|
||||
"type": "string",
|
||||
"example": "fcm_token_example"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Worker": {
|
||||
@ -5093,7 +5219,6 @@
|
||||
"type": "string",
|
||||
"example": "Certified infant caregiver and housekeeper with excellent cooking skills."
|
||||
},
|
||||
|
||||
"verified": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
@ -5128,7 +5253,11 @@
|
||||
},
|
||||
"gender": {
|
||||
"type": "string",
|
||||
"enum": ["male", "female", "other"],
|
||||
"enum": [
|
||||
"male",
|
||||
"female",
|
||||
"other"
|
||||
],
|
||||
"example": "male"
|
||||
},
|
||||
"preferred_location": {
|
||||
|
||||
@ -428,7 +428,7 @@ export default function WorkerManagement({ workers }) {
|
||||
{selectedWorker?.verified ? 'Verified' : 'Pending'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-teal-100 text-xs font-semibold mt-0.5">{selectedWorker?.nationality} • ID #{selectedWorker?.id}</p>
|
||||
<p className="text-teal-100 text-xs font-semibold mt-0.5">{selectedWorker?.nationality}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -714,6 +714,134 @@ export default function WorkerManagement({ workers }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Passport Details */}
|
||||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm space-y-4">
|
||||
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Passport Details</h3>
|
||||
{(() => {
|
||||
const passportDoc = selectedWorker?.documents?.find(d => d.type === 'passport');
|
||||
if (!passportDoc) {
|
||||
return (
|
||||
<div className="text-center py-6 bg-slate-50 rounded-xl border border-slate-100 border-dashed text-slate-400 text-xs font-bold">
|
||||
No passport details available.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="p-4 bg-slate-50 rounded-xl border border-slate-100 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
|
||||
<span className="font-mono font-bold text-slate-800">{passportDoc.ocr_data?.passport_number || passportDoc.number || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Given Names</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.given_names || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Birth</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_birth || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Nationality</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.nationality || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issuing Country</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.issuing_country || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Birth</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.place_of_birth || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Issue</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_issue || passportDoc.issue_date || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Expiry</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_expiry || passportDoc.expiry_date || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{passportDoc.file_path && (
|
||||
<div className="pt-2 border-t border-slate-200/60 flex justify-end">
|
||||
<a
|
||||
href={`/storage/${passportDoc.file_path}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center space-x-1 px-3 py-1.5 bg-white border border-slate-200 rounded-lg text-[10px] font-black uppercase tracking-wider text-[#0F6E56] hover:bg-slate-50 transition-colors shadow-sm"
|
||||
>
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
<span>View Passport File</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Visa Details */}
|
||||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm space-y-4">
|
||||
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Visa Details</h3>
|
||||
{(() => {
|
||||
const visaDoc = selectedWorker?.documents?.find(d => d.type === 'visa');
|
||||
if (!visaDoc) {
|
||||
return (
|
||||
<div className="text-center py-6 bg-slate-50 rounded-xl border border-slate-100 border-dashed text-slate-400 text-xs font-bold">
|
||||
No visa details available.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="p-4 bg-slate-50 rounded-xl border border-slate-100 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">File Number</span>
|
||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.file_number || visaDoc.number || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">ID Number</span>
|
||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.id_number || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issue Date</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.issue_date || visaDoc.issue_date || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Expiry Date</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.expiry_date || visaDoc.expiry_date || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
|
||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.passport_number || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Issue</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.place_of_issue || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sponsor</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.sponsor || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{visaDoc.file_path && (
|
||||
<div className="pt-2 border-t border-slate-200/60 flex justify-end">
|
||||
<a
|
||||
href={`/storage/${visaDoc.file_path}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center space-x-1 px-3 py-1.5 bg-white border border-slate-200 rounded-lg text-[10px] font-black uppercase tracking-wider text-[#0F6E56] hover:bg-slate-50 transition-colors shadow-sm"
|
||||
>
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
<span>View Visa File</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Compliance Notes */}
|
||||
<div className="space-y-2 bg-yellow-50/60 p-4 border border-yellow-200 rounded-2xl">
|
||||
<label className="text-[10px] font-black text-yellow-800 uppercase tracking-widest flex items-center gap-1">
|
||||
|
||||
@ -74,9 +74,7 @@
|
||||
Route::get('/workers/dashboard/views', [WorkerProfileController::class, 'getProfileViews']);
|
||||
Route::get('/workers/dashboard', [WorkerProfileController::class, 'getDashboard']);
|
||||
|
||||
// Document Upload (post-registration OCR processing)
|
||||
Route::post('/workers/register/passport', [WorkerAuthController::class, 'registerPassport']);
|
||||
Route::post('/workers/register/visa', [WorkerAuthController::class, 'registerVisa']);
|
||||
|
||||
|
||||
|
||||
// Job Offers Management
|
||||
|
||||
208
tests/Feature/AdminWorkerManagementTest.php
Normal file
208
tests/Feature/AdminWorkerManagementTest.php
Normal file
@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Worker;
|
||||
use App\Models\Skill;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminWorkerManagementTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected $admin;
|
||||
protected $worker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->admin = User::create([
|
||||
'name' => 'System Admin',
|
||||
'email' => 'admin@marketplace.com',
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'admin',
|
||||
]);
|
||||
|
||||
$this->worker = Worker::create([
|
||||
'name' => 'Jane Helper',
|
||||
'phone' => '+971520000000',
|
||||
'email' => 'jane@helper.com',
|
||||
'password' => bcrypt('password'),
|
||||
'salary' => 1500,
|
||||
'experience' => '2 years',
|
||||
'nationality' => 'Philippine',
|
||||
'gender' => 'Female',
|
||||
'age' => 25,
|
||||
'in_country' => true,
|
||||
'visa_status' => 'Tourist Visa',
|
||||
'preferred_job_type' => 'live-in',
|
||||
'live_in_out' => 'live_in',
|
||||
'preferred_location' => 'Dubai Marina',
|
||||
'language' => 'English, Tagalog',
|
||||
'availability' => 'Available Now',
|
||||
'status' => 'active',
|
||||
'religion' => 'Christian',
|
||||
'bio' => 'Experienced helper',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test admin can list workers.
|
||||
*/
|
||||
public function test_admin_can_list_workers()
|
||||
{
|
||||
$response = $this->actingAs($this->admin)
|
||||
->withSession(['user' => $this->admin])
|
||||
->get('/admin/workers');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee('Jane Helper');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test admin can update worker profile with full registration fields.
|
||||
*/
|
||||
public function test_admin_can_update_worker_profile()
|
||||
{
|
||||
$skill1 = Skill::create(['name' => 'cooking']);
|
||||
|
||||
$response = $this->actingAs($this->admin)
|
||||
->withSession(['user' => $this->admin])
|
||||
->post("/admin/workers/{$this->worker->id}/update-profile", [
|
||||
'name' => 'Jane Helper Edited',
|
||||
'phone' => '+971529999999',
|
||||
'salary' => 1800,
|
||||
'experience' => '4 years',
|
||||
'nationality' => 'Philippine',
|
||||
'gender' => 'Female',
|
||||
'age' => 27,
|
||||
'in_country' => 'false',
|
||||
'visa_status' => null,
|
||||
'preferred_job_type' => 'part-time',
|
||||
'live_in_out' => 'live_out',
|
||||
'preferred_location' => 'JLT',
|
||||
'language' => 'English, Tagalog, Arabic',
|
||||
'skills' => 'cooking, cleaning, childcare',
|
||||
]);
|
||||
|
||||
$response->assertStatus(302);
|
||||
|
||||
$worker = $this->worker->fresh();
|
||||
|
||||
$this->assertEquals('Jane Helper Edited', $worker->name);
|
||||
$this->assertEquals('+971529999999', $worker->phone);
|
||||
$this->assertEquals(1800, (int)$worker->salary);
|
||||
$this->assertEquals('4 years', $worker->experience);
|
||||
$this->assertEquals(27, $worker->age);
|
||||
$this->assertFalse((bool)$worker->in_country);
|
||||
$this->assertEquals('live_out', $worker->live_in_out);
|
||||
$this->assertEquals('JLT', $worker->preferred_location);
|
||||
$this->assertEquals('English, Tagalog, Arabic', $worker->language);
|
||||
|
||||
// Assert skills were synced
|
||||
$this->assertEquals(3, $worker->skills()->count());
|
||||
$this->assertTrue($worker->skills->contains('name', 'cooking'));
|
||||
$this->assertTrue($worker->skills->contains('name', 'cleaning'));
|
||||
$this->assertTrue($worker->skills->contains('name', 'childcare'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test validation on worker update profile.
|
||||
*/
|
||||
public function test_admin_cannot_update_worker_with_invalid_fields()
|
||||
{
|
||||
$response = $this->actingAs($this->admin)
|
||||
->withSession(['user' => $this->admin])
|
||||
->post("/admin/workers/{$this->worker->id}/update-profile", [
|
||||
'name' => '', // Name is required
|
||||
'phone' => '+971529999999',
|
||||
'experience' => '', // Experience is required
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors(['name', 'experience']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test admin can view worker with passport and visa OCR data.
|
||||
*/
|
||||
public function test_admin_can_view_worker_with_document_ocr_data()
|
||||
{
|
||||
// Create a passport document with full OCR data
|
||||
$this->worker->documents()->create([
|
||||
'type' => 'passport',
|
||||
'number' => 'Y34B67890',
|
||||
'issue_date' => '2017-07-05',
|
||||
'expiry_date' => '2022-07-05',
|
||||
'ocr_accuracy' => 99.0,
|
||||
'file_path' => null,
|
||||
'ocr_data' => [
|
||||
'authority' => 'Issuing Authority',
|
||||
'date_of_birth' => '14/05/1990',
|
||||
'date_of_expiry' => '05/07/2022',
|
||||
'date_of_issue' => '05/07/2017',
|
||||
'document_type' => 'P',
|
||||
'given_names' => 'KHALED',
|
||||
'issuing_country' => 'ARE',
|
||||
'nationality' => 'ARE',
|
||||
'passport_number' => 'Y34B67890',
|
||||
'personal_number' => '',
|
||||
'place_of_birth' => 'SHARJAH',
|
||||
'sex' => 'M',
|
||||
'surname' => 'AL-HASHIMI',
|
||||
],
|
||||
]);
|
||||
|
||||
// Create a visa document with full OCR data
|
||||
$this->worker->documents()->create([
|
||||
'type' => 'visa',
|
||||
'number' => '201/2021/2840233',
|
||||
'issue_date' => '2022-02-16',
|
||||
'expiry_date' => '2024-02-15',
|
||||
'ocr_accuracy' => 99.0,
|
||||
'file_path' => null,
|
||||
'ocr_data' => [
|
||||
'accompanied_by' => '9',
|
||||
'expiry_date' => '2024-02-15',
|
||||
'file_number' => '201/2021/2840233',
|
||||
'id_number' => '784198839607839',
|
||||
'issue_date' => '2022-02-16',
|
||||
'name' => 'KHALED AL-HASHIMI',
|
||||
'passport_number' => 'Y34B67890',
|
||||
'place_of_issue' => 'Dubai',
|
||||
'profession' => 'CLEANER',
|
||||
'sponsor' => 'Sponsor Company LLC',
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->admin)
|
||||
->withSession(['user' => $this->admin])
|
||||
->get('/admin/workers');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
// Verify documents are present in the page data
|
||||
$worker = \App\Models\Worker::with('documents')->find($this->worker->id);
|
||||
$passportDoc = $worker->documents->firstWhere('type', 'passport');
|
||||
$visaDoc = $worker->documents->firstWhere('type', 'visa');
|
||||
|
||||
$this->assertNotNull($passportDoc);
|
||||
$this->assertEquals('Y34B67890', $passportDoc->number);
|
||||
$this->assertEquals('KHALED', $passportDoc->ocr_data['given_names']);
|
||||
$this->assertEquals('AL-HASHIMI', $passportDoc->ocr_data['surname']);
|
||||
$this->assertEquals('ARE', $passportDoc->ocr_data['nationality']);
|
||||
$this->assertEquals('ARE', $passportDoc->ocr_data['issuing_country']);
|
||||
$this->assertEquals('SHARJAH', $passportDoc->ocr_data['place_of_birth']);
|
||||
$this->assertEquals('M', $passportDoc->ocr_data['sex']);
|
||||
$this->assertEquals('P', $passportDoc->ocr_data['document_type']);
|
||||
|
||||
$this->assertNotNull($visaDoc);
|
||||
$this->assertEquals('201/2021/2840233', $visaDoc->number);
|
||||
$this->assertEquals('784198839607839', $visaDoc->ocr_data['id_number']);
|
||||
$this->assertEquals('CLEANER', $visaDoc->ocr_data['profession']);
|
||||
$this->assertEquals('Sponsor Company LLC', $visaDoc->ocr_data['sponsor']);
|
||||
$this->assertEquals('Dubai', $visaDoc->ocr_data['place_of_issue']);
|
||||
}
|
||||
}
|
||||
@ -411,54 +411,130 @@ public function test_register_with_new_api_fields()
|
||||
*/
|
||||
public function test_register_with_ocr_extraction()
|
||||
{
|
||||
$apiUrl = config('services.ocr.passport_url', 'http://192.168.0.252:5000/passport');
|
||||
\Illuminate\Support\Facades\Http::fake([
|
||||
$apiUrl => \Illuminate\Support\Facades\Http::response([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'date_of_birth' => '1990-11-07',
|
||||
'date_of_expiry' => '2030-09-06',
|
||||
'given_names' => 'MATAR ALI KHARBASH',
|
||||
'surname' => 'ALSAEDI',
|
||||
'nationality' => 'ARE',
|
||||
'passport_number' => 'SQ0000151',
|
||||
]
|
||||
], 200)
|
||||
]);
|
||||
|
||||
$fakePassport = UploadedFile::fake()->create('passport.pdf', 500, 'application/pdf');
|
||||
|
||||
// Step 1: Register
|
||||
// Consolidated registration with inline passport payload
|
||||
$response = $this->postJson('/api/workers/register', [
|
||||
'name' => 'Temp Name',
|
||||
'phone' => '+971509999999',
|
||||
'salary' => 2500,
|
||||
'password' => 'secret123',
|
||||
'language' => 'HI',
|
||||
'passport' => [
|
||||
'date_of_birth' => '07/11/1990',
|
||||
'date_of_expiry' => '06/09/2030',
|
||||
'date_of_issue' => '06/09/2020',
|
||||
'given_names' => 'MATAR ALI KHARBASH',
|
||||
'surname' => 'ALSAEDI',
|
||||
'nationality' => 'ARE',
|
||||
'passport_number' => 'SQ0000151',
|
||||
]
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$token = $response->json('data.token');
|
||||
|
||||
// Step 2: Upload Passport via separate endpoint
|
||||
$uploadResponse = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
])->postJson('/api/workers/register/passport', [
|
||||
'passport_file' => $fakePassport,
|
||||
'sync_profile' => 'true',
|
||||
]);
|
||||
|
||||
$uploadResponse->assertStatus(200);
|
||||
$workerId = $response->json('data.worker.id');
|
||||
|
||||
$expectedAge = now()->diff(new \DateTime('1990-11-07'))->y;
|
||||
|
||||
$this->assertDatabaseHas('workers', [
|
||||
'id' => $workerId,
|
||||
'name' => 'MATAR ALI KHARBASH ALSAEDI',
|
||||
'phone' => '+971509999999',
|
||||
'nationality' => 'Emirati',
|
||||
'age' => $expectedAge,
|
||||
'verified' => true,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('worker_documents', [
|
||||
'worker_id' => $workerId,
|
||||
'type' => 'passport',
|
||||
'number' => 'SQ0000151',
|
||||
'expiry_date' => '2030-09-06',
|
||||
'issue_date' => '2020-09-06',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test worker passport and visa registration with direct JSON payload from mobile app.
|
||||
*/
|
||||
public function test_register_passport_and_visa_with_direct_json_payload()
|
||||
{
|
||||
$response = $this->postJson('/api/workers/register', [
|
||||
'name' => 'Temp Name',
|
||||
'phone' => '+971508888888',
|
||||
'salary' => 2000,
|
||||
'password' => 'secret123',
|
||||
'language' => 'HI',
|
||||
'passport' => [
|
||||
'authority' => 'Issuing Authority',
|
||||
'date_of_birth' => '14/05/1990',
|
||||
'date_of_expiry' => '05/07/2028',
|
||||
'date_of_issue' => '05/07/2018',
|
||||
'document_type' => 'P',
|
||||
'given_names' => 'Jane',
|
||||
'issuing_country' => 'PHL',
|
||||
'nationality' => 'PHL',
|
||||
'passport_number' => 'Y34B6790',
|
||||
'personal_number' => '28',
|
||||
'place_of_birth' => 'Manila',
|
||||
'sex' => 'F',
|
||||
'surname' => 'Doe',
|
||||
],
|
||||
'visa' => [
|
||||
'accompanied_by' => '9',
|
||||
'expiry_date' => '2027-02-15',
|
||||
'file_number' => '201/2021/2840233',
|
||||
'id_number' => '784198839607839',
|
||||
'issue_date' => '2025-02-16',
|
||||
'name' => 'Jane Doe',
|
||||
'passport_number' => 'Y34B6790',
|
||||
'place_of_issue' => 'Dubai',
|
||||
'profession' => 'CLEANER',
|
||||
'sponsor' => 'Sponsor Name',
|
||||
]
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$workerId = $response->json('data.worker.id');
|
||||
|
||||
$expectedAge = now()->diff(new \DateTime('1990-05-14'))->y;
|
||||
|
||||
$this->assertDatabaseHas('workers', [
|
||||
'id' => $workerId,
|
||||
'name' => 'Jane Doe',
|
||||
'phone' => '+971508888888',
|
||||
'nationality' => 'Filipino',
|
||||
'age' => $expectedAge,
|
||||
'verified' => true,
|
||||
'visa_status' => 'CLEANER',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('worker_documents', [
|
||||
'worker_id' => $workerId,
|
||||
'type' => 'passport',
|
||||
'number' => 'Y34B6790',
|
||||
'expiry_date' => '2028-07-05',
|
||||
'issue_date' => '2018-07-05',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('worker_documents', [
|
||||
'worker_id' => $workerId,
|
||||
'type' => 'visa',
|
||||
'number' => '201/2021/2840233',
|
||||
'expiry_date' => '2027-02-15',
|
||||
'issue_date' => '2025-02-16',
|
||||
]);
|
||||
|
||||
$passportDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'passport')->first();
|
||||
$this->assertNotNull($passportDoc->ocr_data);
|
||||
$this->assertEquals('PHL', $passportDoc->ocr_data['nationality']);
|
||||
$this->assertEquals('Jane', $passportDoc->ocr_data['given_names']);
|
||||
$this->assertEquals('Doe', $passportDoc->ocr_data['surname']);
|
||||
$this->assertEquals('Issuing Authority', $passportDoc->ocr_data['authority']);
|
||||
|
||||
$visaDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'visa')->first();
|
||||
$this->assertNotNull($visaDoc->ocr_data);
|
||||
$this->assertEquals('784198839607839', $visaDoc->ocr_data['id_number']);
|
||||
$this->assertEquals('CLEANER', $visaDoc->ocr_data['profession']);
|
||||
$this->assertEquals('Sponsor Name', $visaDoc->ocr_data['sponsor']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -23,7 +23,7 @@ export default defineConfig({
|
||||
port: 5173,
|
||||
cors: true,
|
||||
hmr: {
|
||||
host: '192.168.0.254',
|
||||
host: '192.168.0.172',
|
||||
},
|
||||
watch: {
|
||||
ignored: ['**/storage/framework/views/**'],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user