assertRedirect(route('login')); }); it('returns 403 when user has no permission to view payrolls', function () { actingAs(createUnauthorizedUser()) ->get(route('payroll.index')) ->assertStatus(403); }); }); describe('Payroll Module - Authorized Actions', function () { beforeEach(function () { $user = createAuthorizedUser([ 'View:Payroll', 'Generate:Payroll', 'Edit:PayrollAdjustment', 'Pay:Payroll', 'Delete:Payroll', 'DeleteAny:Payroll', ]); actingAs($user); }); it('can access payroll index page', function () { get(route('payroll.index')) ->assertOk() ->assertInertia(fn ($page) => $page ->component('admin/finance/payroll/index') ->has('payrolls') ->has('currentMonthPayrolls') ); }); it('can generate payroll', function () { // Mocking artisan call is tricky if we want to check side effects, // but here we check the response and session. postJson(route('payroll.generate')) ->assertRedirect() ->assertSessionHas('success'); }); it('can update payroll adjustments', function () { $payroll = Payroll::factory()->create([ 'base_salary' => 3000000, 'bonus' => 0, 'deduction' => 0, 'total_salary' => 3000000, ]); $data = [ 'adjustments' => [ [ 'type' => SalaryAdjustmentType::BONUS->value, 'amount' => 500000, 'description' => 'Bonus Kerajinan', ], [ 'type' => SalaryAdjustmentType::DEDUCTION->value, 'amount' => 100000, 'description' => 'Potongan Terlambat', ], ], ]; patchJson(route('payroll.update', $payroll), $data) ->assertRedirect() ->assertSessionHas('success'); assertDatabaseHas('payrolls', [ 'id' => $payroll->id, 'bonus' => 500000, 'deduction' => 100000, 'total_salary' => 3400000, // 3M + 500k - 100k ]); assertDatabaseHas('payroll_adjustments', [ 'payroll_id' => $payroll->id, 'description' => 'Bonus Kerajinan', ]); }); it('can toggle paid status', function () { $payroll = Payroll::factory()->create(['is_paid' => false, 'paid_at' => null]); patchJson(route('payroll.togglePaid', $payroll)) ->assertRedirect() ->assertSessionHas('success'); $payroll->refresh(); expect($payroll->is_paid)->toBeTrue(); expect($payroll->paid_at)->not->toBeNull(); patchJson(route('payroll.togglePaid', $payroll)) ->assertRedirect(); $payroll->refresh(); expect($payroll->is_paid)->toBeFalse(); expect($payroll->paid_at)->toBeNull(); }); it('can delete a payroll', function () { $payroll = Payroll::factory()->create(); deleteJson(route('payroll.destroy', $payroll)) ->assertRedirect() ->assertSessionHas('success'); assertSoftDeleted('payrolls', ['id' => $payroll->id]); }); it('can delete payrolls in bulk', function () { $payrolls = Payroll::factory()->count(3)->create(); $ids = $payrolls->pluck('id')->toArray(); deleteJson(route('payroll.bulkDestroy'), ['ids' => $ids]) ->assertRedirect() ->assertSessionHas('success'); foreach ($ids as $id) { assertSoftDeleted('payrolls', ['id' => $id]); } }); }); describe('Payroll Module - Unauthorized Actions', function () { beforeEach(function () { actingAs(createUnauthorizedUser()); }); it('cannot generate payroll without permission', function () { postJson(route('payroll.generate')) ->assertStatus(403); }); it('cannot update payroll adjustment without permission', function () { $payroll = Payroll::factory()->create(); patchJson(route('payroll.update', $payroll), ['adjustments' => []]) ->assertStatus(403); }); it('cannot toggle paid status without permission', function () { $payroll = Payroll::factory()->create(); patchJson(route('payroll.togglePaid', $payroll)) ->assertStatus(403); }); it('cannot delete a payroll without permission', function () { $payroll = Payroll::factory()->create(); deleteJson(route('payroll.destroy', $payroll)) ->assertStatus(403); }); });