feat: add ensureEditable method in EmployeeAdvance model for validation; update EmployeeAdvanceController to handle editability checks; enhance PayEmployeeAdvanceRequest with attribute mapping; refactor EmployeeAdvanceService to utilize transaction handling for create, update, delete, approve, and reject methods

This commit is contained in:
Yoga Pangestu 2026-07-04 00:09:14 +07:00
parent c685f45bcf
commit a8ab96b10b
4 changed files with 90 additions and 97 deletions

View File

@ -5,6 +5,7 @@
use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Finance\ApproveEmployeeAdvanceRequest;
use App\Http\Requests\Admin\Finance\DestroyEmployeeAdvanceRequest;
use App\Http\Requests\Admin\Finance\EmployeeAdvanceRequest;
use App\Http\Requests\Admin\Finance\PayEmployeeAdvanceRequest;
@ -13,6 +14,7 @@
use App\Services\Finance\EmployeeAdvanceService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
@ -49,6 +51,12 @@ public function store(EmployeeAdvanceRequest $request): RedirectResponse
public function update(EmployeeAdvanceRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse
{
try {
$employeeAdvance->ensureEditable();
} catch (ValidationException) {
abort(403);
}
$this->employeeAdvanceService->update($employeeAdvance, $request->validated(), $request->user());
$this->flashUpdated('Kasbon');
@ -58,6 +66,12 @@ public function update(EmployeeAdvanceRequest $request, EmployeeAdvance $employe
public function destroy(DestroyEmployeeAdvanceRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse
{
try {
$employeeAdvance->ensureEditable();
} catch (ValidationException) {
abort(403);
}
$this->employeeAdvanceService->delete($employeeAdvance, $request->user());
$this->flashDeleted('Kasbon');
@ -65,7 +79,7 @@ public function destroy(DestroyEmployeeAdvanceRequest $request, EmployeeAdvance
return redirect()->route('admin.finance.employee_advances.index');
}
public function approve(Request $request, EmployeeAdvance $employeeAdvance): RedirectResponse
public function approve(ApproveEmployeeAdvanceRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse
{
$this->employeeAdvanceService->approve($employeeAdvance, $request->user());

View File

@ -18,4 +18,14 @@ public function rules(): array
'amount' => ['nullable', 'integer', 'min:1'],
];
}
/**
* @return array<string, string>
*/
public function attributes(): array
{
return [
'amount' => 'jumlah',
];
}
}

View File

@ -3,6 +3,7 @@
namespace App\Models;
use App\Enums\EmployeeAdvanceStatus;
use App\Enums\Permission;
use App\Models\Concerns\HasRejection;
use App\Models\Concerns\InteractsWithActivityLog;
use Illuminate\Database\Eloquent\Attributes\Appends;
@ -14,6 +15,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Validation\ValidationException;
#[Guarded(['id'])]
#[Appends([
@ -174,7 +176,24 @@ public function statusLabel(): Attribute
);
}
// 5. Relation
// 5. Other Methods
public function ensureEditable(): void
{
if ($this->status !== EmployeeAdvanceStatus::PENDING) {
throw ValidationException::withMessages([
'status' => 'Kasbon tidak dapat diubah.',
]);
}
}
public static function canSubmit(User $user): bool
{
return $user->can(Permission::EMPLOYEE_ADVANCES_CREATE->value)
&& ! $user->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value)
&& $user->employee !== null;
}
// 6. Relation
public function cashTransaction(): BelongsTo
{
return $this->belongsTo(CashTransaction::class);

View File

@ -3,22 +3,19 @@
namespace App\Services\Finance;
use App\Enums\EmployeeAdvanceStatus;
use App\Enums\Permission;
use App\Enums\Role;
use App\Models\EmployeeAdvance;
use App\Models\EmployeeAdvancePayment;
use App\Models\User;
use App\Services\Concerns\ResolvesAuthenticatedEmployee;
use App\Services\Concerns\RunsInTransaction;
use App\Services\System\PushNotificationService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
class EmployeeAdvanceService
{
use ResolvesAuthenticatedEmployee;
use ResolvesAuthenticatedEmployee, RunsInTransaction;
public function __construct(
private readonly CashService $cashService,
@ -72,8 +69,8 @@ public function create(array $validated, User $user): void
{
$employee = $this->resolveAuthenticatedEmployee($user);
try {
$employeeAdvance = DB::transaction(function () use ($validated, $employee) {
$employeeAdvance = $this->runInTransaction(
function () use ($validated, $employee): EmployeeAdvance {
return EmployeeAdvance::create([
'employee_id' => $employee->id,
'amount' => (int) $validated['amount'],
@ -81,22 +78,13 @@ public function create(array $validated, User $user): void
'due_date' => $validated['due_date'],
'status' => EmployeeAdvanceStatus::PENDING,
]);
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal membuat pengajuan kasbon: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
},
'Gagal membuat pengajuan kasbon',
);
$this->pushNotificationService->sendToRoles(
'💰 Pengajuan Kasbon Baru',
"Karyawan {$user->profil?->full_name} mengajukan kasbon sebesar {$employeeAdvance->amount_formatted} dengan keterangan: {$employeeAdvance->description}.",
"Karyawan {$user->profile?->full_name} mengajukan kasbon sebesar {$employeeAdvance->amount_formatted} dengan keterangan: {$employeeAdvance->description}.",
['owner', 'developer', 'direktur'],
route('admin.finance.employee_advances.index'),
);
@ -104,25 +92,16 @@ public function create(array $validated, User $user): void
public function update(EmployeeAdvance $employeeAdvance, array $validated, User $user): void
{
try {
DB::transaction(function () use ($employeeAdvance, $validated): void {
$this->runInTransaction(
function () use ($employeeAdvance, $validated): void {
$employeeAdvance->update([
'amount' => (int) $validated['amount'],
'description' => $validated['description'],
'due_date' => $validated['due_date'],
]);
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal memperbarui kasbon: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
},
'Gagal memperbarui kasbon',
);
$this->pushNotificationService->sendToRoles(
'✏️ Kasbon Diperbarui',
@ -134,14 +113,19 @@ public function update(EmployeeAdvance $employeeAdvance, array $validated, User
public function delete(EmployeeAdvance $employeeAdvance, User $user): void
{
$amount = $employeeAdvance->amount_formatted;
$amountFormatted = $employeeAdvance->amount_formatted;
$description = $employeeAdvance->description;
$employeeAdvance->delete();
$this->runInTransaction(
function () use ($employeeAdvance): void {
$employeeAdvance->delete();
},
'Gagal menghapus kasbon',
);
$this->pushNotificationService->sendToRoles(
'🗑️ Kasbon Dihapus',
"Kasbon sebesar {$amount} dengan keterangan {$description} telah dihapus.",
"Kasbon sebesar {$amountFormatted} dengan keterangan {$description} telah dihapus.",
['owner', 'developer', 'direktur'],
route('admin.finance.employee_advances.index'),
);
@ -149,8 +133,8 @@ public function delete(EmployeeAdvance $employeeAdvance, User $user): void
public function approve(EmployeeAdvance $employeeAdvance, User $user): void
{
try {
DB::transaction(function () use ($employeeAdvance, $user): void {
$this->runInTransaction(
function () use ($employeeAdvance, $user): void {
$employeeAdvance->loadMissing('employee.user.profile');
$description = sprintf(
@ -171,18 +155,9 @@ public function approve(EmployeeAdvance $employeeAdvance, User $user): void
'verified_at' => now(),
'verified_by_id' => $user->id,
]);
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal menyetujui kasbon: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
},
'Gagal menyetujui kasbon',
);
$employeeAdvance->loadMissing('employee.user');
if ($employeeAdvance->employee?->user_id) {
@ -197,8 +172,8 @@ public function approve(EmployeeAdvance $employeeAdvance, User $user): void
public function reject(EmployeeAdvance $employeeAdvance, string $reason, User $user): void
{
try {
DB::transaction(function () use ($employeeAdvance, $user, $reason): void {
$this->runInTransaction(
function () use ($employeeAdvance, $user, $reason): void {
$employeeAdvance->update([
'status' => EmployeeAdvanceStatus::REJECTED,
'verified_at' => now(),
@ -209,18 +184,9 @@ public function reject(EmployeeAdvance $employeeAdvance, string $reason, User $u
'reason' => $reason,
'rejected_by_id' => $user->id,
]);
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal menolak kasbon: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
},
'Gagal menolak kasbon',
);
$employeeAdvance->loadMissing('employee.user');
if ($employeeAdvance->employee?->user_id) {
@ -235,8 +201,8 @@ public function reject(EmployeeAdvance $employeeAdvance, string $reason, User $u
public function pay(EmployeeAdvance $employeeAdvance, User $user, ?int $payAmount = null): void
{
try {
DB::transaction(function () use ($employeeAdvance, $user, $payAmount): void {
$this->runInTransaction(
function () use ($employeeAdvance, $user, $payAmount): void {
$employeeAdvance->loadMissing('employee.user.profile');
$remaining = $employeeAdvance->amount - $employeeAdvance->paid_amount;
@ -274,18 +240,19 @@ public function pay(EmployeeAdvance $employeeAdvance, User $user, ?int $payAmoun
'description' => $description,
'paid_at' => now(),
]);
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal melunasi kasbon: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
},
'Gagal membayar kasbon',
);
}
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
public function indexPageData(array $tableQuery, User $user, string $status = ''): array
{
return [
'employeeAdvances' => $this->paginateForIndex($tableQuery, $user, $status),
'summary' => $this->outstandingSummary($user),
'authEmployeeId' => $user->employee?->id,
'canSubmit' => EmployeeAdvance::canSubmit($user),
];
}
private function applySorting(Builder $query, string $sort, string $direction): void
@ -298,21 +265,4 @@ private function applySorting(Builder $query, string $sort, string $direction):
$query->latest();
}
public function canSubmit(User $user): bool
{
return $user->can(Permission::EMPLOYEE_ADVANCES_CREATE->value)
&& ! $user->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value)
&& $user->employee !== null;
}
public function indexPageData(array $tableQuery, User $user, string $status = ''): array
{
return [
'employeeAdvances' => $this->paginateForIndex($tableQuery, $user, $status),
'summary' => $this->outstandingSummary($user),
'authEmployeeId' => $user->employee?->id,
'canSubmit' => $this->canSubmit($user),
];
}
}