truncate(); DB::table('mobile_messages')->truncate(); DB::table('mobile_conversation_participants')->truncate(); DB::table('mobile_conversations')->truncate(); DB::table('announcement_reads')->truncate(); DB::table('announcements')->truncate(); DB::table('overtime_requests')->truncate(); DB::table('trip_expenses')->truncate(); DB::table('trips')->truncate(); DB::table('payslips')->truncate(); DB::table('payroll_entries')->truncate(); DB::table('payroll_runs')->truncate(); DB::table('leave_applications')->truncate(); DB::table('attendance_records')->truncate(); DB::table('employees')->truncate(); DB::table('branches')->truncate(); DB::table('users')->truncate(); DB::statement('PRAGMA foreign_keys = ON;'); } private function createClientUser($email = 'crossclient@example.com', $type = 'company', $createdBy = null, $attachBranch = true) { $uniqueEmail = rand(1000, 9999) . '_' . $email; $user = User::create([ 'email' => $uniqueEmail, 'name' => 'Cross Client User', 'password' => bcrypt('password'), 'is_enable_login' => 1, 'status' => 'active', 'slug' => 'cross-client-user-' . rand(1000, 9999), 'type' => $type, 'created_by' => $createdBy ?? 1, ]); $branchId = null; if ($attachBranch) { $branch = Branch::firstOrCreate( ['id' => 1], [ 'name' => 'HQ Branch', 'latitude' => 14.5995, 'longitude' => 120.9842, 'radius' => 500, 'enable_clock_in_out' => 1, 'created_by' => $user->created_by ?? 1, ] ); $branchId = $branch->id; } $employee = Employee::firstOrCreate( ['user_id' => $user->id], [ 'name' => 'Cross Client User', 'email' => $uniqueEmail, 'employee_id' => 'EMP-CROSS-' . rand(1000, 9999), 'branch_id' => $branchId, 'created_by' => $user->created_by ?? 1, ] ); return [$user, $employee]; } public function test_clock_in_requires_assigned_branch() { [$userNoBranch, $empNoBranch] = $this->createClientUser('nobranch@example.com', 'employee', 1, false); $res = $this->actingAs($userNoBranch, 'sanctum') ->postJson('/api/attendance/clock', [ 'action' => 'clock_in', 'latitude' => 14.5995, 'longitude' => 120.9842, ]); $res->assertStatus(422) ->assertJson(['success' => false, 'message' => 'No assigned branch found for employee. Please assign a branch prior to clocking in.']); } // Scenario 1: Web clock-in -> Mobile attendance history public function test_web_clock_in_visible_in_mobile_history() { [$user, $employee] = $this->createClientUser('web_history_sc1@example.com'); AttendanceRecord::create([ 'employee_id' => $user->id, 'branch_id' => $employee->branch_id, 'date' => date('Y-m-d'), 'clock_in' => '08:00:00', 'status' => 'present', 'notes' => 'Web Created Clockin', 'created_by' => $user->id, ]); $res = $this->actingAs($user, 'sanctum') ->getJson('/api/attendance/history'); $res->assertStatus(200) ->assertJson(['success' => true]); $items = $res->json('data'); $this->assertNotEmpty($items); $this->assertEquals('Web Created Clockin', $items[0]['notes']); } // Scenario 2: Mobile leave submission -> Web leave approval (with organizational scoping) public function test_mobile_leave_submission_and_scoped_approval() { [$company, $cEmp] = $this->createClientUser('company_admin_sc2@example.com', 'company', 1); [$otherCompany, $oCEmp] = $this->createClientUser('other_company_sc2@example.com', 'company', 1); [$manager, $mEmp] = $this->createClientUser('manager_same_sc2@example.com', 'manager', $company->id); [$employee, $eEmp] = $this->createClientUser('employee_same_sc2@example.com', 'employee', $company->id); [$otherManager, $oEmp] = $this->createClientUser('other_org_mgr_sc2@example.com', 'manager', $otherCompany->id); // Employee applies $applyRes = $this->actingAs($employee, 'sanctum') ->postJson('/api/leaves/apply', [ 'leave_type' => 'vacation', 'start_date' => date('Y-m-d', strtotime('+5 days')), 'end_date' => date('Y-m-d', strtotime('+7 days')), 'reason' => 'Family Holiday', ]); $applyRes->assertStatus(201); $leaveId = $applyRes->json('data.id'); // Other company manager attempt -> 403 Forbidden $otherApproveRes = $this->actingAs($otherManager, 'sanctum') ->putJson("/api/leaves/applications/{$leaveId}/status", ['status' => 'approved']); $otherApproveRes->assertStatus(403); // Same company manager approval -> 200 OK $sameApproveRes = $this->actingAs($manager, 'sanctum') ->putJson("/api/leaves/applications/{$leaveId}/status", ['status' => 'approved']); $sameApproveRes->assertStatus(200) ->assertJson(['success' => true, 'data' => ['status' => 'approved']]); } // Scenario 3: Reimbursement back-and-forth flow (Mobile submission -> Web approval -> Mobile status verification) public function test_reimbursement_back_and_forth_web_and_mobile() { [$user, $employee] = $this->createClientUser('reimbursement_sc3@example.com'); // 1. Mobile submits a reimbursement claim $res = $this->actingAs($user, 'sanctum') ->postJson('/api/reimbursements', [ 'item_name' => 'Taxi Ride', 'amount' => 350.00, 'category' => 'Transport', 'purchase_date' => date('Y-m-d'), 'notes' => 'Client Visit Transport', ]); $res->assertStatus(201); $trip = Trip::where('employee_id', $user->id)->first(); $this->assertNotNull($trip); // 2. Mobile lists claims -> initial status is pending $listRes = $this->actingAs($user, 'sanctum') ->getJson('/api/reimbursements'); $listRes->assertStatus(200) ->assertJson(['success' => true]); $this->assertEquals('Taxi Ride - Client Visit Transport', $listRes->json('data.0.item_name')); $this->assertEquals('pending', $listRes->json('data.0.status')); // 3. Web Manager/HR approves reimbursement through the actual web controller $webRequest = Request::create( "/hr/trips/{$trip->id}/update-reimbursement-status", 'PUT', ['reimbursement_status' => 'approved'] ); $this->actingAs($user, 'web'); $webResponse = app(TripController::class) ->updateReimbursementStatus($webRequest, $trip->fresh()); $this->assertSame(302, $webResponse->getStatusCode()); $this->assertSame('approved', $trip->fresh()->reimbursement_status); $this->assertSame('approved', $trip->expenses()->first()->status); // 4. Mobile fetches reimbursement list -> status is now approved $updatedRes = $this->actingAs($user, 'sanctum') ->getJson('/api/reimbursements'); $updatedRes->assertStatus(200) ->assertJson(['success' => true]); $this->assertEquals('approved', $updatedRes->json('data.0.status')); } // Scenario 4: Web-approved leave -> Mobile leave status & balance update public function test_web_approved_leave_updates_mobile_balance() { [$user, $employee] = $this->createClientUser('leave_balance_check_sc4@example.com'); LeaveApplication::create([ 'employee_id' => $user->id, 'leave_policy_id' => 1, 'leave_type_id' => 1, 'start_date' => date('Y-m-d'), 'end_date' => date('Y-m-d', strtotime('+2 days')), 'total_days' => 3, 'reason' => 'Medical Rest', 'status' => 'approved', 'created_by' => $user->id, ]); $res = $this->actingAs($user, 'sanctum') ->getJson('/api/leaves/balances'); $res->assertStatus(200) ->assertJson(['success' => true]); } // Scenario 5: Payslip visibility and download URL public function test_payslip_visibility_and_download_url() { [$user, $employee] = $this->createClientUser('payslip_e2e_sc5@example.com'); $runId = DB::table('payroll_runs')->insertGetId([ 'title' => 'January 2026 Payroll', 'payroll_frequency' => 'monthly', 'pay_period_start' => date('Y-01-01'), 'pay_period_end' => date('Y-01-31'), 'pay_date' => date('Y-01-31'), 'status' => 'completed', 'created_by' => $user->id, 'created_at' => now(), 'updated_at' => now(), ]); $entryId = DB::table('payroll_entries')->insertGetId([ 'payroll_run_id' => $runId, 'employee_id' => $employee->id, 'basic_salary' => 40000.00, 'net_pay' => 45000.00, 'created_by' => $user->id, 'created_at' => now(), 'updated_at' => now(), ]); $payslip = Payslip::create([ 'employee_id' => $user->id, 'payroll_entry_id' => $entryId, 'payslip_number' => 'PS-E2E-99', 'pay_period_start' => date('Y-01-01'), 'pay_period_end' => date('Y-01-31'), 'pay_date' => date('Y-01-31'), 'net_payble' => 45000.00, 'basic_salary' => 40000.00, 'salary_month' => 'January 2026', 'created_by' => $user->id, ]); $listRes = $this->actingAs($user, 'sanctum') ->getJson('/api/payslips'); $listRes->assertStatus(200); $downloadRes = $this->actingAs($user, 'sanctum') ->getJson("/api/payslips/{$payslip->id}/download"); $downloadRes->assertStatus(200) ->assertJsonStructure(['success', 'data' => ['pdf_url', 'download_url']]); } // Scenario 6: Conversation creation and two-way real-time messaging back-and-forth between Web and Mobile public function test_conversation_and_messaging_between_two_users() { [$user1, $emp1] = $this->createClientUser('chat_sender_sc6@example.com'); [$user2, $emp2] = $this->createClientUser('chat_receiver_sc6@example.com'); // 1. User 1 creates conversation with User 2 $convRes = $this->actingAs($user1, 'sanctum') ->postJson('/api/conversations', [ 'recipient_id' => $user2->id, 'participant_ids' => [$user2->id], ]); $convRes->assertStatus(201); $convId = $convRes->json('data.id'); // 2. User 1 (Mobile) sends message to User 2 $msgRes1 = $this->actingAs($user1, 'sanctum') ->postJson("/api/conversations/{$convId}/messages", [ 'content' => 'Hello from Mobile Client', ]); $msgRes1->assertStatus(201); // 3. User 2 (Web) fetches messages -> sees User 1's message $getMsgRes2 = $this->actingAs($user2, 'sanctum') ->getJson("/api/conversations/{$convId}/messages"); $getMsgRes2->assertStatus(200) ->assertJson(['success' => true]); $this->assertEquals('Hello from Mobile Client', $getMsgRes2->json('data.0.content')); // 4. User 2 (Web) replies back to User 1 $msgRes2 = $this->actingAs($user2, 'sanctum') ->postJson("/api/conversations/{$convId}/messages", [ 'content' => 'Reply received from Web Client', ]); $msgRes2->assertStatus(201); // 5. User 1 (Mobile) fetches messages -> sees User 2's reply $getMsgRes1 = $this->actingAs($user1, 'sanctum') ->getJson("/api/conversations/{$convId}/messages"); $getMsgRes1->assertStatus(200) ->assertJson(['success' => true]); $messages = $getMsgRes1->json('data'); $this->assertCount(2, $messages); $this->assertEquals('Reply received from Web Client', end($messages)['content']); } // Scenario 7: Announcement listing and read persistence public function test_announcement_read_persistence() { [$user, $employee] = $this->createClientUser('announcement_user_sc7@example.com'); $anc = Announcement::create([ 'title' => 'Company Townhall', 'category' => 'General', 'content' => 'Quarterly sync meeting', 'description' => 'Quarterly sync meeting', 'start_date' => date('Y-m-d'), 'end_date' => date('Y-m-d', strtotime('+7 days')), 'branch_id' => 1, 'department_id' => 1, 'created_by' => $user->id, ]); $listRes = $this->actingAs($user, 'sanctum') ->getJson('/api/announcements'); $listRes->assertStatus(200) ->assertJson(['success' => true]); $this->assertFalse($listRes->json('data.0.is_read')); $readRes = $this->actingAs($user, 'sanctum') ->postJson("/api/announcements/{$anc->id}/read"); $readRes->assertStatus(200) ->assertJson(['success' => true]); $listRes2 = $this->actingAs($user, 'sanctum') ->getJson('/api/announcements'); $this->assertTrue($listRes2->json('data.0.is_read')); } // Scenario 8: Overtime submission and listing public function test_overtime_submission_and_retrieval() { [$user, $employee] = $this->createClientUser('overtime_sc8@example.com'); $postRes = $this->actingAs($user, 'sanctum') ->postJson('/api/overtime', [ 'date' => date('Y-m-d'), 'hours' => 3.5, 'reason' => 'Emergency Deployment', ]); $postRes->assertStatus(201) ->assertJson(['success' => true]); $listRes = $this->actingAs($user, 'sanctum') ->getJson('/api/overtime'); $listRes->assertStatus(200) ->assertJson(['success' => true]); $this->assertEquals(3.5, $listRes->json('data.0.hours')); } // Scenario 9: Profile update and cross-client reflection public function test_profile_update_and_visibility() { [$user, $employee] = $this->createClientUser('profile_sc9@example.com'); $updateRes = $this->actingAs($user, 'sanctum') ->postJson('/api/profile', [ 'name' => 'Updated Profile Name', 'phone' => '+639171234567', ]); $updateRes->assertStatus(200) ->assertJson(['success' => true]); $getRes = $this->actingAs($user, 'sanctum') ->getJson('/api/profile'); $getRes->assertStatus(200) ->assertJson(['success' => true]); $this->assertEquals('Updated Profile Name', $getRes->json('data.name')); } }