feat: Add comprehensive payroll feature tests, refine user scope in Payroll model, and exclude developer users from payroll management.

This commit is contained in:
Yoga Pangestu 2026-01-02 20:40:27 +07:00
parent 48389f4b28
commit 7873701015
4 changed files with 223 additions and 49 deletions

View File

@ -80,13 +80,13 @@ public function columns(): array
', ['row' => $row]))
->html(),
Column::make('Aksi')
->label(function ($row) {
return view('components.actions.table.export-pdf', [
'id' => $row->hash,
])->render();
})
->html(),
// Column::make('Aksi')
// ->label(function ($row) {
// return view('components.actions.table.export-pdf', [
// 'id' => $row->hash,
// ])->render();
// })
// ->html(),
];
}
@ -105,7 +105,9 @@ public function builder(): Builder
'payrolls.created_at'
)
->paid()
->forCurrentUser()
->when(! auth()->user()->hasRole(['Owner', 'Developer']), function ($query) {
return $query->where('payrolls.user_id', auth()->id());
})
->with(['user', 'user.employee']);
}
}

View File

@ -31,8 +31,6 @@ class Payroll extends Component
public PayrollForm $form;
public $payrolls;
public string $method = 'create';
public string $modalTitle = '';
@ -41,9 +39,8 @@ class Payroll extends Component
public function mount(): void
{
$this->loadPayrolls();
$this->users = User::whereHas('employee')
->withoutDeveloper()
->latest()
->get()
->mapWithKeys(fn ($user) => [
@ -52,29 +49,6 @@ public function mount(): void
->toArray();
}
private function loadPayrolls(): void
{
$this->payrolls = PayrollModel::with(['user', 'user.employee'])
->notPaid()
->forCurrentUser()
->where('period_month', now()->format('Y-m'))
->whereHas('user', function ($query) {
$query->active()
->withoutDeveloper();
})
->get();
}
#[On('modal:open')]
public function openModal(string $method, string $modalTitle, ?string $id = null): void
{
$this->resetValidation();
$this->resetErrorBag();
$this->method = $method;
$this->modalTitle = $modalTitle;
}
public function create(): void
{
$this->canOrAbort('manage adjustment');
@ -90,8 +64,6 @@ public function create(): void
],
);
$this->loadPayrolls();
$this->dispatch('refreshDatatable');
$this->toast('Penyesuaian berhasil ditambahkan.');
@ -119,8 +91,6 @@ public function delete(PayrollAdjustment $adjustment): void
$adjustment->delete();
});
$this->loadPayrolls();
$this->dispatch('refreshDatatable');
$this->toast('Penggajian berhasil dihapus.');
Flux::modals()->close();
@ -155,10 +125,33 @@ public function exportPdf(PayrollModel $payroll)
}, $fileName);
}
#[On('modal:open')]
public function openModal(string $method, string $modalTitle, ?string $id = null): void
{
$this->resetValidation();
$this->resetErrorBag();
$this->method = $method;
$this->modalTitle = $modalTitle;
}
public function render(): View
{
$payrolls = PayrollModel::with(['user', 'user.employee'])
->notPaid()
->when(! auth()->user()->hasRole(['Owner', 'Developer']), function ($query) {
return $query->where('payrolls.user_id', auth()->id());
})
->where('period_month', now()->format('Y-m'))
->whereHas('user', function ($query) {
$query->active()
->withoutDeveloper();
})
->get();
return view('livewire.studio.finance.payrolls', [
'pageTitle' => 'Penggajian',
'payrolls' => $payrolls,
]);
}
}

View File

@ -44,16 +44,6 @@ public function notPaid(Builder $query): void
$query->where('is_paid', IsPaid::NOT_PAID->value);
}
#[Scope]
public function forCurrentUser(Builder $query): void
{
$user = auth()->user();
if ($user && ! $user->hasRole(['Owner', 'Developer'])) {
$query->where('user_id', $user->id);
}
}
public function adjustments(): HasMany
{
return $this->hasMany(PayrollAdjustment::class);

View File

@ -0,0 +1,189 @@
<?php
use App\Enums\IsPaid;
use App\Enums\SalaryAdjustmentType;
use App\Livewire\Studio\Finance\Payroll;
use App\Models\Employee;
use App\Models\Payroll as PayrollModel;
use App\Models\PayrollAdjustment;
use App\Models\User;
use Carbon\Carbon;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
beforeEach(function () {
$this->setupUser();
$role = Role::firstOrCreate(['name' => 'Owner']);
Permission::firstOrCreate(['name' => 'view payroll']);
Permission::firstOrCreate(['name' => 'manage adjustment']);
$role->givePermissionTo(['view payroll', 'manage adjustment']);
$this->user->assignRole($role);
});
it('renders the payroll page correctly', function () {
PayrollModel::factory()->create([
'user_id' => $this->user->id,
'period_month' => Carbon::now()->format('Y-m'),
'is_paid' => \App\Enums\IsPaid::NOT_PAID,
]);
$this->actingAs($this->user)
->get(route('studio.finance.payroll.index'))
->assertOk()
->assertSeeLivewire(Payroll::class);
});
it('can open modal for adjustment', function () {
Livewire::actingAs($this->user)
->test(Payroll::class)
->call('openModal', 'create', 'Tambah Penyesuaian')
->assertSet('method', 'create')
->assertSet('modalTitle', 'Tambah Penyesuaian');
});
it('shows validation errors on adjustment form', function () {
Livewire::actingAs($this->user)
->test(Payroll::class)
->call('create')
->assertHasErrors([
'form.user_ids' => 'required',
'form.type' => 'required',
'form.amount' => 'required',
'form.description' => 'required',
]);
});
it('can add a bonus adjustment', function () {
$employeeUser = User::factory()->active()->create();
Employee::factory()->create(['user_id' => $employeeUser->id, 'base_salary' => 5000000]);
// Create payroll for the current month
$payroll = PayrollModel::factory()->create([
'user_id' => $employeeUser->id,
'period_month' => Carbon::now()->format('Y-m'),
'base_salary' => 5000000,
'bonus' => 0,
'deduction' => 0,
'total_salary' => 5000000,
]);
Livewire::actingAs($this->user)
->test(Payroll::class)
->set('form.user_ids', [$employeeUser->id])
->set('form.type', SalaryAdjustmentType::BONUS->value)
->set('form.amount', '500000')
->set('form.description', 'Bonus Performance')
->call('create')
->assertHasNoErrors()
->assertDispatched('refreshDatatable');
$payroll->refresh();
expect($payroll->bonus)->toBe(500000);
expect($payroll->total_salary)->toBe(5500000);
$this->assertDatabaseHas('payroll_adjustments', [
'payroll_id' => $payroll->id,
'type' => SalaryAdjustmentType::BONUS->value,
'amount' => 500000,
'description' => 'Bonus Performance',
]);
});
it('can add a deduction adjustment', function () {
$employeeUser = User::factory()->active()->create();
Employee::factory()->create(['user_id' => $employeeUser->id, 'base_salary' => 5000000]);
// Create payroll for the current month
$payroll = PayrollModel::factory()->create([
'user_id' => $employeeUser->id,
'period_month' => Carbon::now()->format('Y-m'),
'base_salary' => 5000000,
'bonus' => 0,
'deduction' => 0,
'total_salary' => 5000000,
]);
Livewire::actingAs($this->user)
->test(Payroll::class)
->set('form.user_ids', [$employeeUser->id])
->set('form.type', SalaryAdjustmentType::DEDUCTION->value)
->set('form.amount', '200000')
->set('form.description', 'Potongan Keterlambatan')
->call('create')
->assertHasNoErrors()
->assertDispatched('refreshDatatable');
$payroll->refresh();
expect($payroll->deduction)->toBe(200000);
expect($payroll->total_salary)->toBe(4800000);
});
it('can delete an adjustment', function () {
$employeeUser = User::factory()->active()->create();
Employee::factory()->create(['user_id' => $employeeUser->id, 'base_salary' => 5000000]);
$payroll = PayrollModel::factory()->create([
'user_id' => $employeeUser->id,
'period_month' => Carbon::now()->format('Y-m'),
'base_salary' => 5000000,
'bonus' => 500000,
'deduction' => 0,
'total_salary' => 5500000,
]);
$adjustment = PayrollAdjustment::factory()->create([
'payroll_id' => $payroll->id,
'type' => SalaryAdjustmentType::BONUS,
'amount' => 500000,
]);
Livewire::actingAs($this->user)
->test(Payroll::class)
->call('delete', $adjustment->id)
->assertHasNoErrors()
->assertDispatched('refreshDatatable');
$payroll->refresh();
expect($payroll->bonus)->toBe(0);
expect($payroll->total_salary)->toBe(5000000);
$this->assertSoftDeleted('payroll_adjustments', ['id' => $adjustment->id]);
});
it('can export payroll as pdf', function () {
$employeeUser = User::factory()->active()->create();
Employee::factory()->create(['user_id' => $employeeUser->id, 'base_salary' => 5000000]);
$payroll = PayrollModel::factory()->create([
'user_id' => $employeeUser->id,
'period_month' => Carbon::now()->format('Y-m'),
'base_salary' => 5000000,
'is_paid' => IsPaid::NOT_PAID,
]);
Livewire::actingAs($this->user)
->test(Payroll::class)
->call('exportPdf', $payroll)
->assertFileDownloaded();
});
it('cannot manage adjustment without permission', function () {
$this->user->roles()->detach();
$this->user->permissions()->detach();
Livewire::actingAs($this->user)
->test(Payroll::class)
->call('create')
->assertForbidden();
});
it('cannot access payroll page without permission', function () {
$this->user->roles()->detach();
$this->user->permissions()->detach();
$this->actingAs($this->user)
->get(route('studio.finance.payroll.index'))
->assertForbidden();
});