fix: cash record policy, observer, n+1 query
This commit is contained in:
@@ -19,6 +19,9 @@ class CashRecordResource extends Resource
|
|||||||
protected static string|\UnitEnum|null $navigationGroup = 'Keuangan';
|
protected static string|\UnitEnum|null $navigationGroup = 'Keuangan';
|
||||||
protected static ?string $navigationLabel = 'Transaksi';
|
protected static ?string $navigationLabel = 'Transaksi';
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = 'Transaksi';
|
||||||
|
protected static ?string $pluralModelLabel = 'Transaksi';
|
||||||
|
|
||||||
|
|
||||||
public static function form(Schema $form): Schema
|
public static function form(Schema $form): Schema
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Filament\Resources\CashRecords\Tables;
|
namespace App\Filament\Resources\CashRecords\Tables;
|
||||||
|
|
||||||
use App\Models\Approval;
|
|
||||||
use App\Models\ApprovalItem;
|
use App\Models\ApprovalItem;
|
||||||
use App\Models\CashCategory;
|
use App\Models\CashCategory;
|
||||||
use App\Models\CashRecord;
|
use App\Models\CashRecord;
|
||||||
@@ -21,6 +20,7 @@ class CashRecordsTable
|
|||||||
public static function configure(Table $table): Table
|
public static function configure(Table $table): Table
|
||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
|
->modifyQueryUsing(fn ($query) => $query->with(['category', 'creator', 'verifier', 'approval']))
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('date')->label('Tanggal')->date('d M Y')->sortable(),
|
TextColumn::make('date')->label('Tanggal')->date('d M Y')->sortable(),
|
||||||
TextColumn::make('category.name')->label('Kategori'),
|
TextColumn::make('category.name')->label('Kategori'),
|
||||||
@@ -35,9 +35,7 @@ class CashRecordsTable
|
|||||||
if ($record->amount < 500_000 || $record->amount > 2_000_000) {
|
if ($record->amount < 500_000 || $record->amount > 2_000_000) {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
$approval = Approval::where('model_type', CashRecord::class)
|
return match ($record->approval?->status) {
|
||||||
->where('model_id', $record->id)->first();
|
|
||||||
return match ($approval?->status) {
|
|
||||||
'approved' => 'Disetujui',
|
'approved' => 'Disetujui',
|
||||||
'rejected' => 'Ditolak',
|
'rejected' => 'Ditolak',
|
||||||
'pending' => 'Menunggu',
|
'pending' => 'Menunggu',
|
||||||
@@ -74,14 +72,11 @@ class CashRecordsTable
|
|||||||
->visible(function (CashRecord $record): bool {
|
->visible(function (CashRecord $record): bool {
|
||||||
if (! auth()->user()->can('Update:Approval')) return false;
|
if (! auth()->user()->can('Update:Approval')) return false;
|
||||||
if ($record->amount < 500_000 || $record->amount > 2_000_000) return false;
|
if ($record->amount < 500_000 || $record->amount > 2_000_000) return false;
|
||||||
$approval = Approval::where('model_type', CashRecord::class)
|
return $record->approval && $record->approval->status === 'pending';
|
||||||
->where('model_id', $record->id)->first();
|
|
||||||
return $approval && $approval->status === 'pending';
|
|
||||||
})
|
})
|
||||||
->form([Textarea::make('notes')->label('Catatan')->rows(2)])
|
->form([Textarea::make('notes')->label('Catatan')->rows(2)])
|
||||||
->action(function (CashRecord $record, array $data): void {
|
->action(function (CashRecord $record, array $data): void {
|
||||||
$approval = Approval::where('model_type', CashRecord::class)
|
$approval = $record->approval;
|
||||||
->where('model_id', $record->id)->firstOrFail();
|
|
||||||
ApprovalItem::create([
|
ApprovalItem::create([
|
||||||
'approval_id' => $approval->id,
|
'approval_id' => $approval->id,
|
||||||
'user_id' => auth()->id(),
|
'user_id' => auth()->id(),
|
||||||
@@ -108,14 +103,11 @@ class CashRecordsTable
|
|||||||
->visible(function (CashRecord $record): bool {
|
->visible(function (CashRecord $record): bool {
|
||||||
if (! auth()->user()->can('Update:Approval')) return false;
|
if (! auth()->user()->can('Update:Approval')) return false;
|
||||||
if ($record->amount < 500_000 || $record->amount > 2_000_000) return false;
|
if ($record->amount < 500_000 || $record->amount > 2_000_000) return false;
|
||||||
$approval = Approval::where('model_type', CashRecord::class)
|
return $record->approval && $record->approval->status === 'pending';
|
||||||
->where('model_id', $record->id)->first();
|
|
||||||
return $approval && $approval->status === 'pending';
|
|
||||||
})
|
})
|
||||||
->form([Textarea::make('notes')->label('Alasan Penolakan')->required()->rows(2)])
|
->form([Textarea::make('notes')->label('Alasan Penolakan')->required()->rows(2)])
|
||||||
->action(function (CashRecord $record, array $data): void {
|
->action(function (CashRecord $record, array $data): void {
|
||||||
$approval = Approval::where('model_type', CashRecord::class)
|
$approval = $record->approval;
|
||||||
->where('model_id', $record->id)->firstOrFail();
|
|
||||||
ApprovalItem::create([
|
ApprovalItem::create([
|
||||||
'approval_id' => $approval->id,
|
'approval_id' => $approval->id,
|
||||||
'user_id' => auth()->id(),
|
'user_id' => auth()->id(),
|
||||||
@@ -144,20 +136,16 @@ class CashRecordsTable
|
|||||||
->visible(function (CashRecord $record): bool {
|
->visible(function (CashRecord $record): bool {
|
||||||
if (! auth()->user()->can('Update:CashRecord')) return false;
|
if (! auth()->user()->can('Update:CashRecord')) return false;
|
||||||
if ($record->verified_at) return false;
|
if ($record->verified_at) return false;
|
||||||
// Cek threshold
|
|
||||||
if ($record->amount >= 500_000 && $record->amount <= 2_000_000) {
|
if ($record->amount >= 500_000 && $record->amount <= 2_000_000) {
|
||||||
$approval = Approval::where('model_type', CashRecord::class)
|
return $record->approval && $record->approval->status === 'approved';
|
||||||
->where('model_id', $record->id)->first();
|
|
||||||
return $approval && $approval->status === 'approved';
|
|
||||||
}
|
}
|
||||||
if ($record->amount > 2_000_000) {
|
if ($record->amount > 2_000_000) {
|
||||||
$vote = \App\Models\Vote::where('type', 'finance')
|
$vote = \App\Models\Vote::where('type', 'finance')->where('related_id', $record->id)->first();
|
||||||
->where('related_id', $record->id)->first();
|
|
||||||
$total = $vote?->items()->count() ?? 0;
|
$total = $vote?->items()->count() ?? 0;
|
||||||
$approve = $vote?->items()->where('choice', 'approve')->count() ?? 0;
|
$approve = $vote?->items()->where('choice', 'approve')->count() ?? 0;
|
||||||
return $vote && $vote->status === 'closed' && $total > 0 && ($approve / $total) > 0.5;
|
return $vote && $vote->status === 'closed' && $total > 0 && ($approve / $total) > 0.5;
|
||||||
}
|
}
|
||||||
return true; // < 500rb langsung bisa
|
return true;
|
||||||
})
|
})
|
||||||
->action(fn (CashRecord $record) => $record->update([
|
->action(fn (CashRecord $record) => $record->update([
|
||||||
'verified_by' => auth()->id(),
|
'verified_by' => auth()->id(),
|
||||||
|
|||||||
@@ -43,4 +43,9 @@ class CashRecord extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, 'verified_by');
|
return $this->belongsTo(User::class, 'verified_by');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function approval(): \Illuminate\Database\Eloquent\Relations\MorphOne
|
||||||
|
{
|
||||||
|
return $this->morphOne(\App\Models\Approval::class, 'approvable', 'model_type', 'model_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,10 +66,6 @@ class CashRecordObserver
|
|||||||
|
|
||||||
public function deleting(CashRecord $record): void
|
public function deleting(CashRecord $record): void
|
||||||
{
|
{
|
||||||
if ($record->verified_at !== null) {
|
// Guard ditangani di CashRecordPolicy::delete() — tidak perlu throw di sini
|
||||||
throw new \Illuminate\Auth\Access\AuthorizationException(
|
|
||||||
'Transaksi yang sudah diverifikasi tidak dapat dihapus.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class CashRecordPolicy
|
|||||||
|
|
||||||
public function delete(AuthUser $authUser, CashRecord $cashRecord): bool
|
public function delete(AuthUser $authUser, CashRecord $cashRecord): bool
|
||||||
{
|
{
|
||||||
return $authUser->can('Delete:CashRecord') && $cashRecord->verified_at === null;
|
return $authUser->can('Delete:CashRecord');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteAny(AuthUser $authUser): bool
|
public function deleteAny(AuthUser $authUser): bool
|
||||||
|
|||||||
@@ -82,8 +82,14 @@ class CashRecordObserverTest extends TestCase
|
|||||||
$record = $this->makeRecord(100_000);
|
$record = $this->makeRecord(100_000);
|
||||||
$record->update(['verified_by' => $this->user->id, 'verified_at' => now()]);
|
$record->update(['verified_by' => $this->user->id, 'verified_at' => now()]);
|
||||||
|
|
||||||
$this->expectException(\Illuminate\Auth\Access\AuthorizationException::class);
|
// Beri permission Delete:CashRecord agar Policy logic yang diuji (bukan permission check)
|
||||||
|
\Spatie\Permission\Models\Permission::firstOrCreate(['name' => 'Delete:CashRecord', 'guard_name' => 'web']);
|
||||||
|
$this->user->givePermissionTo('Delete:CashRecord');
|
||||||
|
|
||||||
$record->delete();
|
// CashRecordPolicy::delete() return false jika verified_at !== null
|
||||||
|
$this->assertFalse(
|
||||||
|
$this->user->can('delete', $record),
|
||||||
|
'User seharusnya tidak bisa menghapus transaksi yang sudah diverifikasi.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user