From 5c9458f784f8b35107341e48c7410b234736c5c6 Mon Sep 17 00:00:00 2001 From: mohanmd Date: Mon, 15 Jun 2026 14:40:44 +0530 Subject: [PATCH] employer registeration document upload --- .../Api/EmployerAuthController.php | 9 +- .../Employer/EmployerAuthController.php | 91 +++++-- dummy_eid.pdf | 1 + public/swagger.json | 8 +- .../js/Pages/Employer/Auth/CreatePassword.jsx | 19 +- resources/js/Pages/Employer/Auth/Register.jsx | 40 +-- .../Pages/Employer/Auth/RegisterPayment.jsx | 27 +- .../Pages/Employer/Auth/UploadEmiratesId.jsx | 254 ++++++++++++++++++ .../js/Pages/Employer/Auth/VerifyEmail.jsx | 18 +- routes/web.php | 2 + tests/Feature/EmployerMessageApiTest.php | 2 +- tests/Feature/SponsorAuthWebTest.php | 43 +-- 12 files changed, 409 insertions(+), 105 deletions(-) create mode 100644 dummy_eid.pdf create mode 100644 resources/js/Pages/Employer/Auth/UploadEmiratesId.jsx diff --git a/app/Http/Controllers/Api/EmployerAuthController.php b/app/Http/Controllers/Api/EmployerAuthController.php index 7cbaf85..c464a56 100644 --- a/app/Http/Controllers/Api/EmployerAuthController.php +++ b/app/Http/Controllers/Api/EmployerAuthController.php @@ -422,6 +422,11 @@ public function uploadEmiratesId(Request $request) $extractedIdNumber = '784-' . rand(1970, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9); $extractedExpiry = now()->addYears(rand(2, 4))->toDateString(); + // We are not storing this document, just extract data and delete this file + if (file_exists(public_path($filePath))) { + unlink(public_path($filePath)); + } + // Update or create EmployerProfile $profile = EmployerProfile::where('user_id', $user->id)->first(); if (!$profile) { @@ -435,7 +440,7 @@ public function uploadEmiratesId(Request $request) } $profile->update([ - 'emirates_id_front' => $filePath, + 'emirates_id_front' => null, 'emirates_id_number' => $extractedIdNumber, 'emirates_id_expiry' => $extractedExpiry, ]); @@ -443,7 +448,7 @@ public function uploadEmiratesId(Request $request) // Sync with corresponding sponsor record if found if ($sponsor) { $sponsor->update([ - 'emirates_id_file' => $filePath, + 'emirates_id_file' => null, ]); } diff --git a/app/Http/Controllers/Employer/EmployerAuthController.php b/app/Http/Controllers/Employer/EmployerAuthController.php index 73c1903..5495ede 100644 --- a/app/Http/Controllers/Employer/EmployerAuthController.php +++ b/app/Http/Controllers/Employer/EmployerAuthController.php @@ -85,7 +85,6 @@ public function register(Request $request) 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255', 'phone' => 'required|string|regex:/^[0-9]{7,15}$/', - 'emirates_id_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240', ], [ 'phone.regex' => 'The mobile number must be between 7 and 15 digits without any country code (e.g. 501234567).', ]); @@ -99,19 +98,6 @@ public function register(Request $request) ], 409); } - // Store file if provided - $emiratesIdPath = null; - if ($request->hasFile('emirates_id_file')) { - $destinationPath = public_path('uploads/licenses'); - if (!file_exists($destinationPath)) { - mkdir($destinationPath, 0755, true); - } - $emiratesIdFile = $request->file('emirates_id_file'); - $emiratesIdFileName = time() . '_emirates_id_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $emiratesIdFile->getClientOriginalName()); - $emiratesIdFile->move($destinationPath, $emiratesIdFileName); - $emiratesIdPath = 'uploads/licenses/' . $emiratesIdFileName; - } - // 3. Generate 6-digit OTP, hash & store with 10-min expiry $otp = (string) mt_rand(100000, 999999); $hashedOtp = Hash::make($otp); @@ -122,7 +108,6 @@ public function register(Request $request) 'name' => $request->name, 'email' => $request->email, 'phone' => $request->phone, - 'emirates_id_file' => $emiratesIdPath, ], 'employer_otp' => [ 'hash' => $hashedOtp, @@ -208,8 +193,8 @@ public function verifyEmail(Request $request) // Success - mark verified session(['employer_email_verified' => true]); - return redirect()->route('employer.register-payment') - ->with('success', 'Email verified successfully! Please choose a subscription plan to continue.'); + return redirect()->route('employer.upload-emirates-id') + ->with('success', 'Email verified successfully! Please upload your Emirates ID.'); } public function resendOtp(Request $request) @@ -258,13 +243,73 @@ public function resendOtp(Request $request) ]); } - public function showRegisterPayment() + public function showUploadEmiratesId() { if (!session()->has('pending_employer_registration') || !session('employer_email_verified')) { return redirect()->route('employer.register') ->with('error', 'Please complete email verification first.'); } + return Inertia::render('Employer/Auth/UploadEmiratesId', [ + 'email' => session('pending_employer_registration.email'), + ]); + } + + public function uploadEmiratesId(Request $request) + { + if (!session()->has('pending_employer_registration') || !session('employer_email_verified')) { + return redirect()->route('employer.register') + ->with('error', 'Registration session expired. Please start over.'); + } + + $request->validate([ + 'emirates_id_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240', + ], [ + 'emirates_id_file.required' => 'Please upload a clear scan or image of your Emirates ID.', + 'emirates_id_file.mimes' => 'The Emirates ID document must be an image (jpg, jpeg, png) or a PDF file.', + 'emirates_id_file.max' => 'The document must not exceed 10MB.', + ]); + + $filePath = null; + if ($request->hasFile('emirates_id_file')) { + $destinationPath = public_path('uploads/documents'); + if (!file_exists($destinationPath)) { + mkdir($destinationPath, 0755, true); + } + $file = $request->file('emirates_id_file'); + $fileName = time() . '_employer_eid_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName()); + $file->move($destinationPath, $fileName); + $filePath = 'uploads/documents/' . $fileName; + + // OCR Simulated extraction + $extractedIdNumber = '784-' . rand(1970, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9); + $extractedExpiry = now()->addYears(rand(2, 4))->toDateString(); + + // We are not storing this document, just extract data and delete this file + if (file_exists(public_path($filePath))) { + unlink(public_path($filePath)); + } + $filePath = null; // Ensure we don't save the path + } + + $pending = session('pending_employer_registration'); + $pending['emirates_id_file'] = null; + $pending['emirates_id_number'] = $extractedIdNumber; + $pending['emirates_id_expiry'] = $extractedExpiry; + session(['pending_employer_registration' => $pending]); + session(['employer_emirates_id_uploaded' => true]); + + return redirect()->route('employer.register-payment') + ->with('success', 'Emirates ID uploaded and verified successfully.'); + } + + public function showRegisterPayment() + { + if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session('employer_emirates_id_uploaded')) { + return redirect()->route('employer.register') + ->with('error', 'Please complete Emirates ID upload first.'); + } + return Inertia::render('Employer/Auth/RegisterPayment', [ 'email' => session('pending_employer_registration.email'), 'plans' => [ @@ -319,7 +364,7 @@ public function storeRegisterPayment(Request $request) public function showCreatePassword() { - if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session()->has('pending_employer_payment')) { + if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session('employer_emirates_id_uploaded') || !session()->has('pending_employer_payment')) { return redirect()->route('employer.register') ->with('error', 'Please complete payment step first.'); } @@ -344,7 +389,7 @@ public function createPassword(Request $request) 'password.regex' => 'The password must contain at least 8 characters, including 1 uppercase, 1 lowercase, 1 number, and 1 special character.', ]); - if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session()->has('pending_employer_payment')) { + if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session('employer_emirates_id_uploaded') || !session()->has('pending_employer_payment')) { return redirect()->route('employer.register') ->with('error', 'Registration session expired. Please start over.'); } @@ -371,6 +416,9 @@ public function createPassword(Request $request) 'verification_status' => 'approved', 'language' => 'English', 'notifications' => true, + 'emirates_id_front' => null, // We did not store the file + 'emirates_id_number' => $pending['emirates_id_number'] ?? null, + 'emirates_id_expiry' => $pending['emirates_id_expiry'] ?? null, ]); // Create Sponsor @@ -389,7 +437,7 @@ public function createPassword(Request $request) 'otp_verified_at' => now(), 'status' => 'active', 'last_login_at' => now(), - 'emirates_id_file' => $pending['emirates_id_file'] ?? null, + 'emirates_id_file' => null, // We did not store the file ]); // Create active subscription in database @@ -425,6 +473,7 @@ public function createPassword(Request $request) 'pending_employer_registration', 'employer_otp', 'employer_email_verified', + 'employer_emirates_id_uploaded', 'pending_employer_payment' ]); diff --git a/dummy_eid.pdf b/dummy_eid.pdf new file mode 100644 index 0000000..641225e --- /dev/null +++ b/dummy_eid.pdf @@ -0,0 +1 @@ +Dummy PDF Content diff --git a/public/swagger.json b/public/swagger.json index f614856..1d1d261 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -2377,7 +2377,7 @@ "Employer/Auth" ], "summary": "Register Sponsor (Alias)", - "description": "Legacy route matching the simplified sponsor registration (Name, Email, Phone).", + "description": "Legacy route matching the simplified sponsor registration (Name, Email, Phone) (Step 1).", "security": [], "requestBody": { "required": true, @@ -2474,7 +2474,7 @@ "Employer/Auth" ], "summary": "Upload Emirates ID scan & Secure OCR Extract during Onboarding", - "description": "Accepts an email and an Emirates ID scan file, performs automatic OCR extraction of card fields (ID number and expiry), updates the employer's profile fields, and associates the file with their sponsor record.", + "description": "Accepts an email and an Emirates ID scan file, performs automatic OCR extraction of card fields (ID number and expiry), updates the employer's profile fields, and associates the file with their sponsor record (Step 3 - REQUIRED). We are not storing this document, just extracting the data and deleting the file.", "security": [], "requestBody": { "required": true, @@ -2549,7 +2549,7 @@ "Employer/Auth" ], "summary": "Sponsor Subscription Payment", - "description": "Submits a successful plan selection and PayTabs payment confirmation (Step 3).", + "description": "Submits a successful plan selection and PayTabs payment confirmation (Step 4).", "security": [], "requestBody": { "required": true, @@ -2598,7 +2598,7 @@ "Employer/Auth" ], "summary": "Create Sponsor Account Password", - "description": "Configures and finalizes the portal login password to complete registration (Step 4). Returns bearer token.", + "description": "Configures and finalizes the portal login password to complete registration (Step 5). Returns bearer token.", "security": [], "requestBody": { "required": true, diff --git a/resources/js/Pages/Employer/Auth/CreatePassword.jsx b/resources/js/Pages/Employer/Auth/CreatePassword.jsx index c5332f2..a3a5ae6 100644 --- a/resources/js/Pages/Employer/Auth/CreatePassword.jsx +++ b/resources/js/Pages/Employer/Auth/CreatePassword.jsx @@ -108,25 +108,26 @@ export default function CreatePassword() { {[ { step: 1, label: 'Register' }, { step: 2, label: 'Verify' }, - { step: 3, label: 'Payment' }, - { step: 4, label: 'Password' } + { step: 3, label: 'Emirates ID' }, + { step: 4, label: 'Payment' }, + { step: 5, label: 'Password' } ].map((s) => (
{s.step}
- - {s.label} - + {s.step === 5 && ( + + {s.label} + + )}
- {s.step < 4 && ( + {s.step < 5 && (
)} diff --git a/resources/js/Pages/Employer/Auth/Register.jsx b/resources/js/Pages/Employer/Auth/Register.jsx index d1d32e9..2ddf126 100644 --- a/resources/js/Pages/Employer/Auth/Register.jsx +++ b/resources/js/Pages/Employer/Auth/Register.jsx @@ -14,7 +14,6 @@ export default function Register() { name: '', email: '', phone: '', - emirates_id_file: null, }); const [loading, setLoading] = useState(false); @@ -65,9 +64,6 @@ export default function Register() { formData.append('name', data.name); formData.append('email', data.email); formData.append('phone', data.phone); - if (data.emirates_id_file) { - formData.append('emirates_id_file', data.emirates_id_file); - } try { await axios.post('/employer/register', formData, { @@ -102,8 +98,9 @@ export default function Register() { {[ { step: 1, label: 'Register' }, { step: 2, label: 'Verify' }, - { step: 3, label: 'Payment' }, - { step: 4, label: 'Password' } + { step: 3, label: 'Emirates ID' }, + { step: 4, label: 'Payment' }, + { step: 5, label: 'Password' } ].map((s) => (
@@ -116,13 +113,13 @@ export default function Register() { }`}> {s.step}
- - {s.label} - + {s.step === 1 && ( + + {s.label} + + )}
- {s.step < 4 && ( + {s.step < 5 && (
)} @@ -260,25 +257,6 @@ export default function Register() { )}
- {/* Emirates ID File */} -
- - handleInputChange('emirates_id_file', e.target.files[0])} - className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all file:mr-4 file:py-1 file:px-3 file:rounded-lg file:border-0 file:text-xs file:font-semibold file:bg-[#185FA5]/10 file:text-[#185FA5] hover:file:bg-[#185FA5]/20 cursor-pointer ${ - errors.emirates_id_file - ? 'border-red-500 focus:ring-red-500/20 focus:border-red-500' - : 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]' - }`} - /> - {errors.emirates_id_file && ( -

- {Array.isArray(errors.emirates_id_file) ? errors.emirates_id_file[0] : errors.emirates_id_file} -

- )} -
+ + +
+ + + Change Email / Back + +
+ + + + ); +} diff --git a/resources/js/Pages/Employer/Auth/VerifyEmail.jsx b/resources/js/Pages/Employer/Auth/VerifyEmail.jsx index 51fef8f..0ec3db6 100644 --- a/resources/js/Pages/Employer/Auth/VerifyEmail.jsx +++ b/resources/js/Pages/Employer/Auth/VerifyEmail.jsx @@ -93,14 +93,14 @@ export default function VerifyEmail({ email }) { setResending(false); } }; - const renderStepIndicator = () => (
{[ { step: 1, label: 'Register' }, { step: 2, label: 'Verify' }, - { step: 3, label: 'Payment' }, - { step: 4, label: 'Password' } + { step: 3, label: 'Emirates ID' }, + { step: 4, label: 'Payment' }, + { step: 5, label: 'Password' } ].map((s) => (
@@ -113,13 +113,13 @@ export default function VerifyEmail({ email }) { }`}> {s.step}
- - {s.label} - + {s.step === 2 && ( + + {s.label} + + )}
- {s.step < 4 && ( + {s.step < 5 && (
)} diff --git a/routes/web.php b/routes/web.php index 236be15..d656359 100644 --- a/routes/web.php +++ b/routes/web.php @@ -230,6 +230,8 @@ Route::get('/employer/verify-email', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showVerifyEmail'])->name('employer.verify-email'); Route::post('/employer/verify-email', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'verifyEmail'])->name('employer.verify-email.submit')->middleware('throttle:5,1'); Route::post('/employer/resend-otp', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'resendOtp'])->name('employer.resend-otp')->middleware('throttle:3,1'); +Route::get('/employer/upload-emirates-id', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showUploadEmiratesId'])->name('employer.upload-emirates-id'); +Route::post('/employer/upload-emirates-id', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'uploadEmiratesId'])->name('employer.upload-emirates-id.submit'); Route::get('/employer/register-payment', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showRegisterPayment'])->name('employer.register-payment'); Route::post('/employer/register-payment', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'storeRegisterPayment'])->name('employer.register-payment.submit'); Route::get('/employer/create-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showCreatePassword'])->name('employer.create-password'); diff --git a/tests/Feature/EmployerMessageApiTest.php b/tests/Feature/EmployerMessageApiTest.php index d1251e5..5de9da1 100644 --- a/tests/Feature/EmployerMessageApiTest.php +++ b/tests/Feature/EmployerMessageApiTest.php @@ -138,7 +138,7 @@ public function test_employer_can_register() $user = User::where('email', 'alice@example.com')->first(); $profile = EmployerProfile::where('user_id', $user->id)->first(); - $this->assertNotNull($profile->emirates_id_front); + $this->assertNull($profile->emirates_id_front); $this->assertNotNull($profile->emirates_id_number); $this->assertNotNull($profile->emirates_id_expiry); } diff --git a/tests/Feature/SponsorAuthWebTest.php b/tests/Feature/SponsorAuthWebTest.php index 7bf6181..939b888 100644 --- a/tests/Feature/SponsorAuthWebTest.php +++ b/tests/Feature/SponsorAuthWebTest.php @@ -28,42 +28,52 @@ public function test_employer_web_registration_with_emirates_id() 'name' => 'Abdullah Ahmed', 'email' => 'abdullah@example.com', 'phone' => '501234567', - 'emirates_id_file' => $emiratesIdFile, ]); $responseStep1->assertRedirect(route('employer.verify-email')); - // Verify session data and file presence + // Verify session data $pendingReg = session('pending_employer_registration'); $this->assertNotNull($pendingReg); $this->assertEquals('Abdullah Ahmed', $pendingReg['name']); - $this->assertNotNull($pendingReg['emirates_id_file']); - $this->assertStringContainsString('uploads/licenses/', $pendingReg['emirates_id_file']); // Step 2: OTP Verification (using local environment dummy code) $responseStep2 = $this->post('/employer/verify-email', [ 'otp' => '000000', ]); - $responseStep2->assertRedirect(route('employer.register-payment')); + $responseStep2->assertRedirect(route('employer.upload-emirates-id')); $this->assertTrue(session('employer_email_verified')); - // Step 3: Payment Choice - $responseStep3 = $this->post('/employer/register-payment', [ + // Step 3: Emirates ID Upload + $responseStep3 = $this->post('/employer/upload-emirates-id', [ + 'emirates_id_file' => $emiratesIdFile, + ]); + + $responseStep3->assertRedirect(route('employer.register-payment')); + $this->assertTrue(session('employer_emirates_id_uploaded')); + + $pendingRegAfterId = session('pending_employer_registration'); + $this->assertNotNull($pendingRegAfterId['emirates_id_number']); + $this->assertNotNull($pendingRegAfterId['emirates_id_expiry']); + $this->assertNull($pendingRegAfterId['emirates_id_file']); + + // Step 4: Payment Choice + $responseStep4 = $this->post('/employer/register-payment', [ 'plan_id' => 'premium', 'amount_aed' => 199.00, 'paytabs_transaction_id' => 'TRAN_ABC_123', ]); - $responseStep3->assertJson(['message' => 'Payment processed successfully.']); + $responseStep4->assertJson(['message' => 'Payment processed successfully.']); - // Step 4: Create Password & Create Records - $responseStep4 = $this->post('/employer/create-password', [ + // Step 5: Create Password & Create Records + $responseStep5 = $this->post('/employer/create-password', [ 'password' => 'SecurePass123!', 'password_confirmation' => 'SecurePass123!', ]); - $responseStep4->assertRedirect(route('employer.dashboard')); + $responseStep5->assertRedirect(route('employer.dashboard')); // Verify Database Records $this->assertDatabaseHas('users', [ @@ -74,16 +84,17 @@ public function test_employer_web_registration_with_emirates_id() $this->assertDatabaseHas('employer_profiles', [ 'phone' => '501234567', 'company_name' => 'Abdullah Ahmed Household', + 'emirates_id_front' => null, // File is not stored ]); + $profile = EmployerProfile::where('phone', '501234567')->first(); + $this->assertNotNull($profile->emirates_id_number); + $this->assertNotNull($profile->emirates_id_expiry); + $this->assertDatabaseHas('sponsors', [ 'email' => 'abdullah@example.com', 'mobile' => '501234567', + 'emirates_id_file' => null, // File is not stored ]); - - // Check Sponsor has the Emirates ID path matching the session stored file - $sponsor = Sponsor::where('email', 'abdullah@example.com')->first(); - $this->assertNotNull($sponsor->emirates_id_file); - $this->assertEquals($pendingReg['emirates_id_file'], $sponsor->emirates_id_file); } }