Add Access group admin surfaces, booking soft deletes, refund crash fix
Access group (Filament):
- StaffResource: manage users with an admin-tier role, gated by manage_staff
- CustomerResource: read-only view of role-less users, gated by view_customers
- RoleResource: edit permissions per role (fixed role set), gated by manage_roles
- ManageAppSettings: tabbed General/Booking settings page that reads/writes
real .env keys via new EnvFileWriter (no parallel DB settings table, so
BookingService/config('booking.*') stay unchanged)
- Moved Access above Catalog in the nav group order
- New permissions: manage_staff, manage_roles, view_customers, manage_settings
Booking soft deletes:
- bookings.deleted_at + SoftDeletes on the Booking model
- BookingPolicy::delete (manage_bookings, cancelled/expired only) and
::restore (manage_bookings)
- DeleteBookingTableAction/RestoreBookingTableAction + TrashedFilter on
BookingsTable, using authorize() so the policy is enforced at call time,
not just cosmetically hidden
Refund crash fix:
- ProcessRefundAction passed a nullable $payment->booking into
RefundBookingAction's non-nullable Booking param — a soft-deleted
booking's payment reaching the refund picker was an uncaught TypeError.
Excluded such payments from the picker and added a defensive guard.
- Same unguarded $event->payment->booking / $event->refund->payment->booking
pattern fixed in the MarkBookingPaid/MarkBookingRefunded queued listeners.
289 tests passing.
This commit is contained in:
@@ -243,3 +243,96 @@ test('the detail page\'s assign driver action is hidden for a pending_payment bo
|
||||
->assertActionHidden('assignDriver')
|
||||
->assertActionEnabled('cancel');
|
||||
});
|
||||
|
||||
test('the delete action is hidden for a pending_payment or confirmed booking, even with manage_bookings', function () {
|
||||
$pending = Booking::factory()->create(['status' => BookingStatus::PendingPayment]);
|
||||
$confirmed = Booking::factory()->create(['status' => BookingStatus::Confirmed]);
|
||||
|
||||
// authorize('delete') ties visibility straight to BookingPolicy::delete
|
||||
// (status + permission combined) — a non-terminal booking never shows
|
||||
// this button at all, rather than a dead disabled one.
|
||||
Livewire::test(ListBookings::class)
|
||||
->assertTableActionHidden('delete', $pending)
|
||||
->assertTableActionHidden('delete', $confirmed);
|
||||
});
|
||||
|
||||
test('the delete action is visible and enabled for a cancelled or expired booking', function () {
|
||||
$cancelled = Booking::factory()->create(['status' => BookingStatus::Cancelled]);
|
||||
$expired = Booking::factory()->create(['status' => BookingStatus::Expired]);
|
||||
|
||||
Livewire::test(ListBookings::class)
|
||||
->assertTableActionVisible('delete', $cancelled)
|
||||
->assertTableActionEnabled('delete', $cancelled)
|
||||
->assertTableActionVisible('delete', $expired)
|
||||
->assertTableActionEnabled('delete', $expired);
|
||||
});
|
||||
|
||||
test('the delete action is hidden from a user without manage_bookings', function () {
|
||||
$stranger = User::factory()->create();
|
||||
$booking = Booking::factory()->create(['status' => BookingStatus::Cancelled]);
|
||||
|
||||
$this->actingAs($stranger);
|
||||
|
||||
Livewire::test(ListBookings::class)
|
||||
->assertTableActionHidden('delete', $booking);
|
||||
});
|
||||
|
||||
test('deleting a cancelled booking soft-deletes it', function () {
|
||||
$booking = Booking::factory()->create(['status' => BookingStatus::Cancelled]);
|
||||
|
||||
Livewire::test(ListBookings::class)
|
||||
->callTableAction('delete', $booking)
|
||||
->assertSuccessful();
|
||||
|
||||
expect(Booking::find($booking->id))->toBeNull();
|
||||
expect(Booking::withTrashed()->find($booking->id))->not->toBeNull();
|
||||
expect(Booking::withTrashed()->find($booking->id)->trashed())->toBeTrue();
|
||||
});
|
||||
|
||||
test('a soft-deleted booking is hidden from the default list but visible via the trashed filter', function () {
|
||||
$active = Booking::factory()->create();
|
||||
$deleted = Booking::factory()->create();
|
||||
$deleted->delete();
|
||||
|
||||
Livewire::test(ListBookings::class)
|
||||
->assertCanSeeTableRecords([$active])
|
||||
->assertCanNotSeeTableRecords([$deleted])
|
||||
->filterTable('trashed', true)
|
||||
->assertCanSeeTableRecords([$active, $deleted]);
|
||||
});
|
||||
|
||||
test('the restore action is only visible for a trashed booking', function () {
|
||||
$active = Booking::factory()->create();
|
||||
$deleted = Booking::factory()->create();
|
||||
$deleted->delete();
|
||||
|
||||
Livewire::test(ListBookings::class)
|
||||
->filterTable('trashed', true)
|
||||
->assertTableActionHidden('restore', $active)
|
||||
->assertTableActionVisible('restore', $deleted);
|
||||
});
|
||||
|
||||
test('restoring a deleted booking brings it back', function () {
|
||||
$booking = Booking::factory()->create();
|
||||
$booking->delete();
|
||||
|
||||
Livewire::test(ListBookings::class)
|
||||
->filterTable('trashed', true)
|
||||
->callTableAction('restore', $booking)
|
||||
->assertSuccessful();
|
||||
|
||||
expect(Booking::find($booking->id))->not->toBeNull();
|
||||
expect(Booking::find($booking->id)->trashed())->toBeFalse();
|
||||
});
|
||||
|
||||
test('the restore action is hidden from a user without manage_bookings', function () {
|
||||
$stranger = User::factory()->create();
|
||||
$booking = Booking::factory()->create();
|
||||
$booking->delete();
|
||||
|
||||
$this->actingAs($stranger);
|
||||
|
||||
Livewire::test(ListBookings::class)
|
||||
->filterTable('trashed', true)
|
||||
->assertTableActionHidden('restore', $booking);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user