orWhere('type', 'company')->first() ?? User::first(); $employee = Employee::where('user_id', $user->id)->first() ?? Employee::first(); AttendanceRecord::where('employee_id', $user->id)->orWhere('employee_id', $employee->user_id)->delete(); $service = new AttendanceService(); // 1. Web Clock In $resIn = $service->processClock($user, $employee, [ 'action' => 'clock_in', 'source' => 'web', 'latitude' => 14.5995, 'longitude' => 120.9842, ]); expect($resIn['success'])->toBeTrue(); // 2. Mobile Break In $resBreakIn = $service->processClock($user, $employee, [ 'action' => 'break_in', 'source' => 'mobile', 'latitude' => 14.5995, 'longitude' => 120.9842, ]); expect($resBreakIn['success'])->toBeTrue(); // 3. Mobile Break Out $resBreakOut = $service->processClock($user, $employee, [ 'action' => 'break_out', 'source' => 'mobile', 'latitude' => 14.5995, 'longitude' => 120.9842, ]); expect($resBreakOut['success'])->toBeTrue(); // 4. Web Clock Out $resOut = $service->processClock($user, $employee, [ 'action' => 'clock_out', 'source' => 'web', 'latitude' => 14.5995, 'longitude' => 120.9842, ]); expect($resOut['success'])->toBeTrue(); $rec = AttendanceRecord::where('employee_id', $user->id)->whereDate('date', date('Y-m-d'))->first(); expect($rec)->not->toBeNull(); expect($rec->clock_in_source)->toBe('web'); expect($rec->clock_out_source)->toBe('web'); $breaksCount = AttendanceBreak::where('attendance_record_id', $rec->id)->count(); expect($breaksCount)->toBeGreaterThanOrEqual(1); }); test('ATT-MOB-WEB-001: mobile clock-in followed by web clock-out creates unified record with source metadata', function () { updateSetting('working_days', json_encode([0,1,2,3,4,5,6])); $user = User::where('email', 'admin@example.com')->orWhere('type', 'company')->first() ?? User::first(); $employee = Employee::where('user_id', $user->id)->first() ?? Employee::first(); AttendanceRecord::where('employee_id', $user->id)->orWhere('employee_id', $employee->user_id)->delete(); $service = new AttendanceService(); // Mobile Clock In $resIn = $service->processClock($user, $employee, [ 'action' => 'clock_in', 'source' => 'mobile', 'latitude' => 14.5995, 'longitude' => 120.9842, ]); expect($resIn['success'])->toBeTrue(); // Web Clock Out $resOut = $service->processClock($user, $employee, [ 'action' => 'clock_out', 'source' => 'web', 'latitude' => 14.5995, 'longitude' => 120.9842, ]); expect($resOut['success'])->toBeTrue(); $rec = AttendanceRecord::where('employee_id', $user->id)->whereDate('date', date('Y-m-d'))->first(); expect($rec)->not->toBeNull(); expect($rec->clock_in_source)->toBe('mobile'); expect($rec->clock_out_source)->toBe('web'); expect($rec->shift_id)->not->toBeNull(); }); test('web calendar query payload includes mobile record and clock_in_source', function () { updateSetting('working_days', json_encode([0,1,2,3,4,5,6])); $user = User::where('email', 'admin@example.com')->orWhere('type', 'company')->first() ?? User::first(); $employee = Employee::where('user_id', $user->id)->first() ?? Employee::first(); AttendanceRecord::where('employee_id', $user->id)->orWhere('employee_id', $employee->user_id)->delete(); $service = new AttendanceService(); $service->processClock($user, $employee, [ 'action' => 'clock_in', 'source' => 'mobile', 'latitude' => 14.5995, 'longitude' => 120.9842, ]); $controller = new \App\Http\Controllers\AttendanceRecordController(); $request = \Illuminate\Http\Request::create('/hr/attendance-records/calendar', 'GET', [ 'month' => date('n'), 'year' => date('Y'), ]); // Authenticate as user $this->actingAs($user); $response = $controller->calendar($request); expect($response)->not->toBeNull(); }); test('duplicate clock in is rejected with 422', function () { updateSetting('working_days', json_encode([0,1,2,3,4,5,6])); $user = User::where('email', 'admin@example.com')->orWhere('type', 'company')->first() ?? User::first(); $employee = Employee::where('user_id', $user->id)->first() ?? Employee::first(); AttendanceRecord::where('employee_id', $user->id)->orWhere('employee_id', $employee->user_id)->delete(); $service = new AttendanceService(); $res1 = $service->processClock($user, $employee, ['action' => 'clock_in', 'source' => 'mobile', 'latitude' => 14.5995, 'longitude' => 120.9842]); expect($res1['success'])->toBeTrue(); $res2 = $service->processClock($user, $employee, ['action' => 'clock_in', 'source' => 'mobile', 'latitude' => 14.5995, 'longitude' => 120.9842]); expect($res2['success'])->toBeFalse(); expect($res2['status_code'])->toBe(422); }); test('clock out without clock in is rejected with 422', function () { updateSetting('working_days', json_encode([0,1,2,3,4,5,6])); $user = User::where('email', 'admin@example.com')->orWhere('type', 'company')->first() ?? User::first(); $employee = Employee::where('user_id', $user->id)->first() ?? Employee::first(); AttendanceRecord::where('employee_id', $user->id)->orWhere('employee_id', $employee->user_id)->delete(); $service = new AttendanceService(); $res = $service->processClock($user, $employee, ['action' => 'clock_out', 'source' => 'mobile', 'latitude' => 14.5995, 'longitude' => 120.9842]); expect($res['success'])->toBeFalse(); expect($res['status_code'])->toBe(422); });