service = new PayrollService(); // 1. Seed statutory brackets (new StatutoryBracketSeeder())->run(); // 2. Create Company user $this->company = User::create([ 'name' => 'Test Company', 'email' => 'company@test.com', 'password' => bcrypt('password'), 'type' => 'company', ]); } /** * Create an employee with a salary structure. */ private function createEmployee(string $name, float $salary, string $frequency = 'semi-monthly', bool $isDaily = false, array $restDays = ['saturday', 'sunday']) { $user = User::create([ 'name' => $name, 'email' => strtolower(str_replace(' ', '', $name)) . '@test.com', 'password' => bcrypt('password'), 'type' => 'employee', ]); $employee = Employee::create([ 'user_id' => $user->id, 'employee_id' => 'EMP-' . rand(1000, 9999), 'date_of_joining' => '2026-01-01', 'employee_status' => 'active', 'rest_days' => $restDays, 'created_by' => $this->company->id, ]); EmployeeSalary::create([ 'employee_id' => $user->id, 'basic_salary' => $salary, 'pay_frequency' => $frequency, 'is_active' => true, 'calculation_status' => 'calculated', 'is_time_exempt' => false, 'is_taxable' => false, 'created_by' => $this->company->id, ]); return $employee; } /** * Helper to seed present attendances for work days in June 1-15 2026. */ private function seedAttendancesForPeriod($employeeId, array $excludeDates = []) { $startDate = Carbon::parse('2026-06-01'); $endDate = Carbon::parse('2026-06-15'); for ($date = $startDate->copy(); $date->lte($endDate); $date->addDay()) { $dateStr = $date->toDateString(); if ($date->isWeekend() || in_array($dateStr, $excludeDates)) { continue; } AttendanceRecord::create([ 'employee_id' => $employeeId, 'date' => $dateStr, 'clock_in' => '08:00:00', 'clock_out' => '17:00:00', 'total_hours' => 9.0, // 8 work hours + 1 break 'status' => 'present', 'is_absent' => false, 'is_rest_day' => false, 'created_by' => $this->company->id, ]); } } /** * Scenario 1: Regular Holiday worked/unworked (200% / 100%) */ public function test_scenario_1_regular_holiday() { // 1. Worked Regular Holiday $employeeWorked = $this->createEmployee('Worked Regular', 26000.00); // Daily rate = 1000 Holiday::create([ 'name' => 'Regular Holiday Test', 'start_date' => '2026-06-12', 'end_date' => '2026-06-12', 'holiday_type' => 'regular', 'category' => 'National', 'created_by' => $this->company->id, ]); // Attendance on holiday (June 12 is Friday, working day) AttendanceRecord::create([ 'employee_id' => $employeeWorked->user_id, 'date' => '2026-06-12', 'clock_in' => '08:00:00', 'clock_out' => '17:00:00', 'total_hours' => 9.0, // 8 work hours + 1 break 'status' => 'present', 'is_absent' => false, 'is_rest_day' => false, 'created_by' => $this->company->id, ]); $compWorked = $this->service->calculateForPeriod($employeeWorked, '2026-06-01', '2026-06-15 23:59:59'); // Worked holiday premium should be 100% of daily rate (1,000.00 PHP) $holidayEarning = collect($compWorked['earnings'])->firstWhere('name', 'Holiday Pay'); $this->assertEquals(1000.00, (float)$holidayEarning['amount']); // 2. Unworked Regular Holiday $employeeUnworked = $this->createEmployee('Unworked Regular', 26000.00); $compUnworked = $this->service->calculateForPeriod($employeeUnworked, '2026-06-01', '2026-06-15 23:59:59'); // Unworked holiday should still get 100% of daily rate (1,000.00 PHP) as Holiday Pay $holidayEarningUnworked = collect($compUnworked['earnings'])->firstWhere('name', 'Holiday Pay'); $this->assertEquals(1000.00, (float)$holidayEarningUnworked['amount']); } /** * Scenario 2: Special Non-Working Holiday worked/unworked (130% / 0%) */ public function test_scenario_2_special_holiday() { // 1. Worked Special Holiday $employeeWorked = $this->createEmployee('Worked Special', 26000.00); // Daily rate = 1000 Holiday::create([ 'name' => 'Special Holiday Test', 'start_date' => '2026-06-12', 'end_date' => '2026-06-12', 'holiday_type' => 'special_non_working', 'category' => 'National', 'created_by' => $this->company->id, ]); AttendanceRecord::create([ 'employee_id' => $employeeWorked->user_id, 'date' => '2026-06-12', 'clock_in' => '08:00:00', 'clock_out' => '17:00:00', 'total_hours' => 9.0, 'status' => 'present', 'is_absent' => false, 'is_rest_day' => false, 'created_by' => $this->company->id, ]); $compWorked = $this->service->calculateForPeriod($employeeWorked, '2026-06-01', '2026-06-15 23:59:59'); // Worked special holiday premium should be 30% of daily rate (300.00 PHP) $holidayEarning = collect($compWorked['earnings'])->firstWhere('name', 'Holiday Pay'); $this->assertEquals(300.00, (float)$holidayEarning['amount']); // 2. Unworked Special Holiday $employeeUnworked = $this->createEmployee('Unworked Special', 26000.00); // Seed present attendance for all other working days $this->seedAttendancesForPeriod($employeeUnworked->user_id, ['2026-06-12']); $compUnworked = $this->service->calculateForPeriod($employeeUnworked, '2026-06-01', '2026-06-15 23:59:59'); // Unworked special holiday gets 0 holiday pay and no absence deduction (it is a non-working day) $holidayEarningUnworked = collect($compUnworked['earnings'])->firstWhere('name', 'Holiday Pay'); $this->assertEquals(0.00, (float)$holidayEarningUnworked['amount']); $this->assertEquals(0, $compUnworked['summary']['absences']); } /** * Scenario 3: Rest Day worked */ public function test_scenario_3_rest_day() { $employee = $this->createEmployee('Worked RestDay', 26000.00); // Daily rate = 1000 // June 14 is a Sunday (Rest Day) AttendanceRecord::create([ 'employee_id' => $employee->user_id, 'date' => '2026-06-14', 'clock_in' => '08:00:00', 'clock_out' => '17:00:00', 'total_hours' => 9.0, 'status' => 'present', 'is_absent' => false, 'is_rest_day' => true, 'created_by' => $this->company->id, ]); $comp = $this->service->calculateForPeriod($employee, '2026-06-01', '2026-06-15 23:59:59'); // Per client instruction comment: "Rest Day Premium removed per client request" // It does not add extra premium to the earnings, but treats it as a day worked. $this->assertEquals(1, $comp['summary']['days_worked']); } /** * Scenario 4: Compounding Premium (Regular Holiday on Rest Day = 260%) */ public function test_scenario_4_compounding_premium() { $employee = $this->createEmployee('Compounded RestHoliday', 26000.00); // Daily rate = 1000 // Create Regular Holiday on June 14 (Sunday / Rest Day) Holiday::create([ 'name' => 'Regular Holiday on Rest Day', 'start_date' => '2026-06-14', 'end_date' => '2026-06-14', 'holiday_type' => 'regular', 'category' => 'National', 'created_by' => $this->company->id, ]); AttendanceRecord::create([ 'employee_id' => $employee->user_id, 'date' => '2026-06-14', 'clock_in' => '08:00:00', 'clock_out' => '17:00:00', 'total_hours' => 9.0, 'status' => 'present', 'is_absent' => false, 'is_rest_day' => true, 'created_by' => $this->company->id, ]); $comp = $this->service->calculateForPeriod($employee, '2026-06-01', '2026-06-15 23:59:59'); // Regular Holiday on Rest Day worked premium: 160% of daily rate (1,600.00 PHP) $holidayEarning = collect($comp['earnings'])->firstWhere('name', 'Holiday Pay'); $this->assertEquals(1600.00, (float)$holidayEarning['amount']); } /** * Scenario 5: Night Differential & Overtime with break deductions (meal breaks) */ public function test_scenario_5_night_diff_and_ot() { $employee = $this->createEmployee('OT and ND Employee', 26000.00); // Hourly rate = 1000 / 8 = 125.00 // June 10 (Wednesday) AttendanceRecord::create([ 'employee_id' => $employee->user_id, 'date' => '2026-06-10', 'clock_in' => '14:00:00', 'clock_out' => '23:00:00', 'total_hours' => 9.0, 'night_diff_hours' => 4.0, // 4 hours in ND bracket (10 PM onwards) 'status' => 'present', 'is_absent' => false, 'is_rest_day' => false, 'created_by' => $this->company->id, ]); // Approved OT application for 2 hours OvertimeApplication::create([ 'user_id' => $employee->user_id, 'date' => '2026-06-10', 'requested_hours' => 2.0, 'approved_hours' => 2.0, 'status' => 'approved', 'reason' => 'Busy day', ]); $comp = $this->service->calculateForPeriod($employee, '2026-06-01', '2026-06-15 23:59:59'); // ND calculation: (night_diff_hours - 1.5 shift meal break deduction) * hourly_rate * 10% // (4.0 - 1.5) = 2.5 hours * (125.00 * 0.10) = 2.5 * 12.50 = 31.25 PHP $ndEarning = collect($comp['earnings'])->firstWhere('name', 'Night Differential'); $this->assertEquals(31.25, (float)$ndEarning['amount']); // OT calculation: 2.0 hours * (125.00 * 1.25 multiplier) = 2.0 * 156.25 = 312.50 PHP $otEarning = collect($comp['earnings'])->firstWhere('name', 'Overtime (Regular)'); $this->assertEquals(312.50, (float)$otEarning['amount']); } /** * Scenario 6: Paid vs Unpaid Leaves (LWOP) proration */ public function test_scenario_6_leaves() { $employee = $this->createEmployee('Leave Employee', 26000.00); // Daily rate = 1000 $paidType = LeaveType::create([ 'name' => 'Paid Vacation Leave', 'code' => 'PVL', 'is_paid' => true, 'created_by' => $this->company->id, ]); $paidPolicy = LeavePolicy::create([ 'name' => 'Paid Policy', 'leave_type_id' => $paidType->id, 'requires_approval' => true, 'created_by' => $this->company->id, ]); $unpaidType = LeaveType::create([ 'name' => 'Unpaid Leave (LWOP)', 'code' => 'LWOP', 'is_paid' => false, 'created_by' => $this->company->id, ]); $unpaidPolicy = LeavePolicy::create([ 'name' => 'Unpaid Policy', 'leave_type_id' => $unpaidType->id, 'requires_approval' => true, 'created_by' => $this->company->id, ]); // 1. Paid Leave on June 8 LeaveApplication::create([ 'employee_id' => $employee->user_id, 'leave_type_id' => $paidType->id, 'leave_policy_id' => $paidPolicy->id, 'start_date' => '2026-06-08', 'end_date' => '2026-06-08', 'total_days' => 1, 'status' => 'approved', 'reason' => 'Vacation', 'created_by' => $this->company->id, ]); // 2. Unpaid Leave on June 9 LeaveApplication::create([ 'employee_id' => $employee->user_id, 'leave_type_id' => $unpaidType->id, 'leave_policy_id' => $unpaidPolicy->id, 'start_date' => '2026-06-09', 'end_date' => '2026-06-09', 'total_days' => 1, 'status' => 'approved', 'reason' => 'LWOP reason', 'created_by' => $this->company->id, ]); // Seed present attendances for all other working days $this->seedAttendancesForPeriod($employee->user_id, ['2026-06-08', '2026-06-09']); $comp = $this->service->calculateForPeriod($employee, '2026-06-01', '2026-06-15 23:59:59'); // Cut-off salary = 13,000.00 // Paid leaves amount = 1,000.00 PHP (shown as separate earning line) // Unpaid leave = counts as 1 absence, leading to 1,000.00 PHP deduction $paidLeaveEarning = collect($comp['earnings'])->firstWhere('name', 'Paid Leaves'); $this->assertEquals(1000.00, (float)$paidLeaveEarning['amount']); $absenceDeduction = collect($comp['deductions'])->firstWhere('name', 'Absences / LWOP'); $this->assertEquals(1000.00, (float)$absenceDeduction['amount']); // Basic salary = Cut-off salary - paid leaves amount - unworked holiday (0) = 13,000 - 1,000 = 12,000.00 $this->assertEquals(12000.00, $comp['basic_salary']); } /** * Scenario 7: Daily Wage vs Fixed Monthly Wage calculation proration */ public function test_scenario_7_wage_types() { // Monthly wage: 26000.00 (Cut-off salary = 13000.00) $employeeMonthly = $this->createEmployee('Monthly Employee', 26000.00, 'semi-monthly', false); // Daily wage: 1000.00 basic_salary, pay_frequency = daily $employeeDaily = $this->createEmployee('Daily Employee', 1000.00, 'daily', true); // Attendance on 5 days in period $dates = ['2026-06-01', '2026-06-02', '2026-06-03', '2026-06-04', '2026-06-05']; foreach ($dates as $date) { AttendanceRecord::create([ 'employee_id' => $employeeMonthly->user_id, 'date' => $date, 'clock_in' => '08:00:00', 'clock_out' => '17:00:00', 'total_hours' => 9.0, 'status' => 'present', 'is_absent' => false, 'created_by' => $this->company->id, ]); AttendanceRecord::create([ 'employee_id' => $employeeDaily->user_id, 'date' => $date, 'clock_in' => '08:00:00', 'clock_out' => '17:00:00', 'total_hours' => 9.0, 'status' => 'present', 'is_absent' => false, 'created_by' => $this->company->id, ]); } $compMonthly = $this->service->calculateForPeriod($employeeMonthly, '2026-06-01', '2026-06-15 23:59:59'); $compDaily = $this->service->calculateForPeriod($employeeDaily, '2026-06-01', '2026-06-15 23:59:59'); // Monthly earner gets full cut-off salary (13,000.00) minus absences deduction // expected working days = 11. worked = 5. Absences = 6 * 1,000 = 6,000.00 deduction. // basic_salary remains 13,000.00 (since no leaves/holidays), net total earnings is 13,000.00, net pay is 13,000 - 6,000 = 7,000 (excluding statutory). $this->assertEquals(13000.00, $compMonthly['basic_salary']); $absenceDeduction = collect($compMonthly['deductions'])->firstWhere('name', 'Absences / LWOP'); $this->assertEquals(6000.00, (float)$absenceDeduction['amount']); // Daily earner gets strictly daily rate * days worked = 1000.00 * 5 = 5,000.00 PHP basic salary // with no absences deduction $this->assertEquals(5000.00, $compDaily['basic_salary']); $absenceDeductionDaily = collect($compDaily['deductions'])->firstWhere('name', 'Absences / LWOP'); $this->assertNull($absenceDeductionDaily); } }