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

View File

@ -18,4 +18,14 @@ public function rules(): array
'amount' => ['nullable', 'integer', 'min:1'], '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; namespace App\Models;
use App\Enums\EmployeeAdvanceStatus; use App\Enums\EmployeeAdvanceStatus;
use App\Enums\Permission;
use App\Models\Concerns\HasRejection; use App\Models\Concerns\HasRejection;
use App\Models\Concerns\InteractsWithActivityLog; use App\Models\Concerns\InteractsWithActivityLog;
use Illuminate\Database\Eloquent\Attributes\Appends; use Illuminate\Database\Eloquent\Attributes\Appends;
@ -14,6 +15,7 @@
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Validation\ValidationException;
#[Guarded(['id'])] #[Guarded(['id'])]
#[Appends([ #[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 public function cashTransaction(): BelongsTo
{ {
return $this->belongsTo(CashTransaction::class); return $this->belongsTo(CashTransaction::class);

View File

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