forgetCachedPermissions(); $this->seed(RolePermissionSeeder::class); }); /* |-------------------------------------------------------------------------- | HELPERS |-------------------------------------------------------------------------- */ function notifUserWithRole(string $role, bool $active = true): User { $user = User::factory()->create(['is_active' => $active]); $user->assignRole($role); return $user; } function notifEmployeeWithRole(string $role = 'Kasir'): array { $user = notifUserWithRole($role); $employee = Employee::factory()->create(['user_id' => $user->id]); return ['user' => $user, 'employee' => $employee]; } /* |-------------------------------------------------------------------------- | NOTIFICATION SERVICE — UNIT TESTS |-------------------------------------------------------------------------- */ test('NotificationService creates in-app notification for users with matching role', function () { $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); notifUserWithRole('Kasir'); // should NOT receive NotificationService::notify( roles: ['Owner', 'Developer'], title: 'Test Title', body: 'Test Body.', url: '/test-url' ); expect($developer->notifications()->count())->toBe(1); expect($owner->notifications()->count())->toBe(1); $notification = $developer->notifications()->first(); expect($notification->title)->toBe('Test Title') ->and($notification->body)->toBe('Test Body.') ->and($notification->url)->toBe('/test-url') ->and($notification->is_read)->toBeFalse(); }); test('NotificationService does not notify inactive users', function () { notifUserWithRole('Developer', active: false); $active = notifUserWithRole('Developer'); NotificationService::notify( roles: ['Developer'], title: 'Test', body: 'Body.', url: '/test' ); expect($active->notifications()->count())->toBe(1); expect(User::where('is_active', false)->first()->notifications()->count())->toBe(0); }); test('NotificationService dispatches WebPushNotification to each user', function () { Notification::fake(); $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); NotificationService::notify( roles: ['Developer', 'Owner'], title: 'Push Title', body: 'Push Body.', url: '/push' ); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($owner, WebPushNotification::class); }); test('NotificationService includes additionalUser when not already in role-based list', function () { $kasir = notifUserWithRole('Kasir'); $developer = notifUserWithRole('Developer'); NotificationService::notify( roles: ['Developer'], title: 'Title', body: 'Body.', url: '/url', additionalUser: $kasir ); expect($kasir->notifications()->count())->toBe(1); expect($developer->notifications()->count())->toBe(1); }); test('NotificationService does not duplicate additionalUser already in role list', function () { $developer = notifUserWithRole('Developer'); NotificationService::notify( roles: ['Developer'], title: 'Title', body: 'Body.', url: '/url', additionalUser: $developer ); expect($developer->notifications()->count())->toBe(1); }); test('NotificationService handles null additionalUser', function () { $developer = notifUserWithRole('Developer'); NotificationService::notify( roles: ['Developer'], title: 'Title', body: 'Body.', url: '/url', additionalUser: null ); expect($developer->notifications()->count())->toBe(1); }); test('NotificationService notifies multiple users across multiple roles', function () { $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); $direktur = notifUserWithRole('Direktur'); $adminToko = notifUserWithRole('Admin Toko'); notifUserWithRole('Kasir'); // should NOT receive NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], title: 'Multi Role', body: 'Body.', url: '/multi' ); expect($developer->notifications()->count())->toBe(1); expect($owner->notifications()->count())->toBe(1); expect($direktur->notifications()->count())->toBe(1); expect($adminToko->notifications()->count())->toBe(1); }); /* |-------------------------------------------------------------------------- | PRODUCT NOTIFICATIONS |-------------------------------------------------------------------------- */ test('ProductService create sends notification to correct roles', function () { Notification::fake(); $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); $kasir = notifUserWithRole('Kasir'); $this->actingAs($developer); $service = new ProductService; $product = $service->create([ 'name' => 'Test Product', 'description' => 'A test product', 'status' => 'active', 'category_ids' => [], 'use_same_price' => false, 'variants' => [ [ 'name' => 'Default', 'stock' => 10, 'reject_stock' => 0, 'retail_stock' => 5, 'prices' => [ ['type' => 'retail', 'price' => 50000], ], ], ], ]); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($owner, WebPushNotification::class); Notification::assertNothingSentTo($kasir); expect($developer->notifications()->count())->toBe(1); expect($developer->notifications()->first()->title)->toBe('Produk Baru'); }); test('ProductService update sends notification to correct roles', function () { Notification::fake(); $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); $adminToko = notifUserWithRole('Admin Toko'); $this->actingAs($developer); $service = new ProductService; $product = $service->create([ 'name' => 'Original Product', 'description' => 'Desc', 'status' => 'active', 'category_ids' => [], 'use_same_price' => false, 'variants' => [ [ 'name' => 'Var 1', 'stock' => 10, 'reject_stock' => 0, 'retail_stock' => 5, 'prices' => [ ['type' => 'retail', 'price' => 50000], ], ], ], ]); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($owner, WebPushNotification::class); Notification::fake(); $product = $service->update($product, [ 'name' => 'Updated Product', 'description' => 'Desc updated', 'status' => 'active', 'category_ids' => [], 'use_same_price' => false, 'variants' => [ [ 'id' => $product->productVariants->first()->id, 'name' => 'Var 1', 'stock' => 20, 'reject_stock' => 0, 'retail_stock' => 10, 'prices' => [ ['type' => 'retail', 'price' => 60000], ], ], ], ]); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($owner, WebPushNotification::class); Notification::assertSentTo($adminToko, WebPushNotification::class); }); /* |-------------------------------------------------------------------------- | CASH ACCOUNT NOTIFICATIONS |-------------------------------------------------------------------------- */ test('CashTransactionService deposit sends notification to correct roles', function () { Notification::fake(); $cashAccount = CashAccount::factory()->create(['balance' => 1000000]); $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); $kasir = notifUserWithRole('Kasir'); $this->actingAs($developer); $service = new CashTransactionService; $service->deposit([ 'amount' => 500000, 'description' => 'Setoran modal', ]); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($owner, WebPushNotification::class); Notification::assertNothingSentTo($kasir); expect($developer->notifications()->first()->title)->toBe('Setoran Kas Toko'); }); test('CashTransactionService withdrawal sends notification to correct roles', function () { Notification::fake(); $cashAccount = CashAccount::factory()->create(['balance' => 1000000]); $developer = notifUserWithRole('Developer'); $direktur = notifUserWithRole('Direktur'); $this->actingAs($developer); $service = new CashTransactionService; $service->withdrawal([ 'amount' => 200000, 'description' => 'Penarikan kas', ]); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($direktur, WebPushNotification::class); expect($developer->notifications()->first()->title)->toBe('Penarikan Kas Toko'); }); test('CashTransactionService update sends notification to correct roles', function () { Notification::fake(); $cashAccount = CashAccount::factory()->create(['balance' => 1000000]); $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); $this->actingAs($developer); $service = new CashTransactionService; $transaction = $service->deposit([ 'amount' => 500000, 'description' => 'Initial deposit', ]); Notification::fake(); $service->update($transaction, [ 'amount' => 600000, 'description' => 'Updated deposit', ]); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($owner, WebPushNotification::class); expect($developer->notifications()->where('title', 'Transaksi Kas Diperbarui')->count())->toBe(1); }); test('CashTransactionService destroy sends notification to correct roles', function () { Notification::fake(); $cashAccount = CashAccount::factory()->create(['balance' => 1000000]); $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); $this->actingAs($developer); $service = new CashTransactionService; $transaction = $service->deposit([ 'amount' => 500000, 'description' => 'To be deleted', ]); Notification::fake(); $service->destroy($transaction); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($owner, WebPushNotification::class); expect($developer->notifications()->where('title', 'Transaksi Kas Dihapus')->count())->toBe(1); }); /* |-------------------------------------------------------------------------- | EXPENSE NOTIFICATIONS |-------------------------------------------------------------------------- */ test('ExpenseService create sends notification to correct roles', function () { Notification::fake(); CashAccount::factory()->create(['balance' => 5000000]); $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); $kasir = notifUserWithRole('Kasir'); $this->actingAs($developer); $service = new ExpenseService; $expense = $service->create([ 'amount' => 100000, 'description' => 'Office supplies', ]); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($owner, WebPushNotification::class); Notification::assertNothingSentTo($kasir); expect($developer->notifications()->first()->title)->toBe('Pengeluaran Baru'); }); test('ExpenseService update sends notification to correct roles', function () { Notification::fake(); CashAccount::factory()->create(['balance' => 5000000]); $developer = notifUserWithRole('Developer'); $adminToko = notifUserWithRole('Admin Toko'); $this->actingAs($developer); $service = new ExpenseService; $expense = $service->create([ 'amount' => 100000, 'description' => 'Original', ]); Notification::fake(); $service->update($expense, [ 'amount' => 150000, 'description' => 'Updated expense', ]); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($adminToko, WebPushNotification::class); expect($developer->notifications()->where('title', 'Pengeluaran Diperbarui')->count())->toBe(1); }); test('ExpenseService delete sends notification to correct roles', function () { Notification::fake(); CashAccount::factory()->create(['balance' => 5000000]); $developer = notifUserWithRole('Developer'); $direktur = notifUserWithRole('Direktur'); $this->actingAs($developer); $service = new ExpenseService; $expense = $service->create([ 'amount' => 100000, 'description' => 'To delete', ]); Notification::fake(); $service->delete($expense); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($direktur, WebPushNotification::class); expect($developer->notifications()->where('title', 'Pengeluaran Dihapus')->count())->toBe(1); }); /* |-------------------------------------------------------------------------- | EMPLOYEE ADVANCE (KASBON) NOTIFICATIONS |-------------------------------------------------------------------------- */ test('EmployeeAdvanceService create sends notification including the requesting employee', function () { Notification::fake(); CashAccount::factory()->create(['balance' => 5000000]); $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); $kasbonData = notifEmployeeWithRole('Kasir'); $employee = $kasbonData['employee']; $kasbonUser = $kasbonData['user']; $this->actingAs($kasbonUser); $service = new EmployeeAdvanceService; $advance = $service->create([ 'amount' => 500000, 'description' => 'Emergency need', 'due_date' => now()->addDays(30)->toDateString(), ]); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($owner, WebPushNotification::class); Notification::assertSentTo($kasbonUser, WebPushNotification::class); expect($kasbonUser->notifications()->first()->title)->toBe('Kasbon Baru'); }); test('EmployeeAdvanceService approve sends notification including the employee', function () { Notification::fake(); CashAccount::factory()->create(['balance' => 5000000]); $developer = notifUserWithRole('Developer'); $kasbonData = notifEmployeeWithRole('Kasir'); $employee = $kasbonData['employee']; $kasbonUser = $kasbonData['user']; $this->actingAs($kasbonUser); $service = new EmployeeAdvanceService; $advance = $service->create([ 'amount' => 300000, 'description' => 'Advance', 'due_date' => now()->addDays(14)->toDateString(), ]); Notification::fake(); $admin = notifUserWithRole('Admin Toko'); $this->actingAs($admin); $service->approve($advance); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($kasbonUser, WebPushNotification::class); expect($kasbonUser->notifications()->where('title', 'Kasbon Dikeluarkan')->count())->toBe(1); }); test('EmployeeAdvanceService pay sends notification including the employee', function () { Notification::fake(); CashAccount::factory()->create(['balance' => 5000000]); $developer = notifUserWithRole('Developer'); $kasbonData = notifEmployeeWithRole('Kasir'); $employee = $kasbonData['employee']; $kasbonUser = $kasbonData['user']; $this->actingAs($kasbonUser); $service = new EmployeeAdvanceService; $advance = $service->create([ 'amount' => 200000, 'description' => 'To pay', 'due_date' => now()->addDays(7)->toDateString(), ]); $admin = notifUserWithRole('Admin Toko'); $this->actingAs($admin); $service->approve($advance); Notification::fake(); $service->pay($advance); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($kasbonUser, WebPushNotification::class); expect($kasbonUser->notifications()->where('title', 'Kasbon Dikembalikan')->count())->toBe(1); }); /* |-------------------------------------------------------------------------- | PAYROLL NOTIFICATIONS |-------------------------------------------------------------------------- */ test('PayrollPeriodService pay sends notification including the employee', function () { Notification::fake(); CashAccount::factory()->create(['balance' => 50000000]); $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); $employeeData = notifEmployeeWithRole('Kasir'); $employee = $employeeData['employee']; $employeeUser = $employeeData['user']; $period = PayrollPeriod::factory()->create(['year' => 2026, 'month' => 7, 'status' => 'open']); $payroll = Payroll::factory()->create([ 'payroll_period_id' => $period->id, 'employee_id' => $employee->id, 'base_salary' => 3000000, 'bonus_amount' => 0, 'deduction_amount' => 0, 'total_amount' => 3000000, 'status' => 'unpaid', ]); $this->actingAs($developer); $service = new PayrollPeriodService; $service->pay($payroll); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($owner, WebPushNotification::class); Notification::assertSentTo($employeeUser, WebPushNotification::class); expect($employeeUser->notifications()->where('title', 'Gaji Dibayar')->count())->toBe(1); }); test('PayrollPeriodService cancel sends notification including the employee', function () { Notification::fake(); $developer = notifUserWithRole('Developer'); $direktur = notifUserWithRole('Direktur'); $employeeData = notifEmployeeWithRole('Kasir'); $employee = $employeeData['employee']; $employeeUser = $employeeData['user']; $period = PayrollPeriod::factory()->create(['year' => 2026, 'month' => 8, 'status' => 'open']); $payroll = Payroll::factory()->create([ 'payroll_period_id' => $period->id, 'employee_id' => $employee->id, 'base_salary' => 3000000, 'bonus_amount' => 0, 'deduction_amount' => 0, 'total_amount' => 3000000, 'status' => 'unpaid', ]); $this->actingAs($developer); $service = new PayrollPeriodService; $service->cancel($payroll); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($direktur, WebPushNotification::class); Notification::assertSentTo($employeeUser, WebPushNotification::class); expect($employeeUser->notifications()->where('title', 'Gaji Dibatalkan')->count())->toBe(1); }); /* |-------------------------------------------------------------------------- | LEAVE REQUEST NOTIFICATIONS |-------------------------------------------------------------------------- */ test('LeaveRequestService create sends notification including the requesting employee', function () { Notification::fake(); $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); $leaveData = notifEmployeeWithRole('Kasir'); $employee = $leaveData['employee']; $leaveUser = $leaveData['user']; $this->actingAs($leaveUser); $service = new LeaveRequestService; $leaveRequest = $service->create([ 'start_date' => now()->addDays(5)->toDateString(), 'end_date' => now()->addDays(7)->toDateString(), ]); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($owner, WebPushNotification::class); Notification::assertSentTo($leaveUser, WebPushNotification::class); expect($leaveUser->notifications()->first()->title)->toBe('Pengajuan Cuti Baru'); }); test('LeaveRequestService approve sends notification including the employee', function () { Notification::fake(); $developer = notifUserWithRole('Developer'); $leaveData = notifEmployeeWithRole('Kasir'); $employee = $leaveData['employee']; $leaveUser = $leaveData['user']; $this->actingAs($leaveUser); $service = new LeaveRequestService; $leaveRequest = $service->create([ 'start_date' => now()->addDays(10)->toDateString(), 'end_date' => now()->addDays(12)->toDateString(), ]); Notification::fake(); $admin = notifUserWithRole('Admin Toko'); $this->actingAs($admin); $service->approve($leaveRequest); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($leaveUser, WebPushNotification::class); expect($leaveUser->notifications()->where('title', 'Cuti Disetujui')->count())->toBe(1); }); test('LeaveRequestService reject sends notification including the employee', function () { Notification::fake(); $developer = notifUserWithRole('Developer'); $leaveData = notifEmployeeWithRole('Kasir'); $employee = $leaveData['employee']; $leaveUser = $leaveData['user']; $this->actingAs($leaveUser); $service = new LeaveRequestService; $leaveRequest = $service->create([ 'start_date' => now()->addDays(15)->toDateString(), 'end_date' => now()->addDays(17)->toDateString(), ]); Notification::fake(); $admin = notifUserWithRole('Admin Toko'); $this->actingAs($admin); $service->reject($leaveRequest); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($leaveUser, WebPushNotification::class); expect($leaveUser->notifications()->where('title', 'Cuti Ditolak')->count())->toBe(1); }); /* |-------------------------------------------------------------------------- | ATTENDANCE NOTIFICATIONS |-------------------------------------------------------------------------- */ test('AttendanceService checkIn sends notification to Developer, Owner, Direktur', function () { Notification::fake(); $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); $direktur = notifUserWithRole('Direktur'); $adminToko = notifUserWithRole('Admin Toko'); $attData = notifEmployeeWithRole('Kasir'); $employee = $attData['employee']; $attendant = $attData['user']; $this->actingAs($attendant); $service = new AttendanceService; $attendance = $service->checkIn([ 'latitude' => -6.200000, 'longitude' => 106.816666, ]); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($owner, WebPushNotification::class); Notification::assertSentTo($direktur, WebPushNotification::class); Notification::assertNothingSentTo($adminToko); expect($developer->notifications()->first()->title)->toBe('Presensi Masuk'); }); test('AttendanceService checkOut sends notification to Developer, Owner, Direktur', function () { Notification::fake(); $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); $direktur = notifUserWithRole('Direktur'); $adminToko = notifUserWithRole('Admin Toko'); $attData = notifEmployeeWithRole('Kasir'); $employee = $attData['employee']; $attendant = $attData['user']; $this->actingAs($attendant); $service = new AttendanceService; $attendance = $service->checkIn([ 'latitude' => -6.200000, 'longitude' => 106.816666, ]); Notification::fake(); $attendance = $service->checkOut($attendance, [ 'latitude' => -6.200100, 'longitude' => 106.816700, ]); Notification::assertSentTo($developer, WebPushNotification::class); Notification::assertSentTo($owner, WebPushNotification::class); Notification::assertSentTo($direktur, WebPushNotification::class); Notification::assertNothingSentTo($adminToko); expect($developer->notifications()->where('title', 'Presensi Pulang')->count())->toBe(1); }); /* |-------------------------------------------------------------------------- | IN-APP NOTIFICATION RECORDS |-------------------------------------------------------------------------- */ test('services create in-app notification records in database', function () { $developer = notifUserWithRole('Developer'); $owner = notifUserWithRole('Owner'); NotificationService::notify( roles: ['Developer', 'Owner'], title: 'DB Test', body: 'Body here.', url: '/db-test' ); $this->assertDatabaseHas('notifications', [ 'user_id' => $developer->id, 'title' => 'DB Test', 'body' => 'Body here.', 'url' => '/db-test', 'is_read' => false, ]); $this->assertDatabaseHas('notifications', [ 'user_id' => $owner->id, 'title' => 'DB Test', ]); }); test('ProductService create creates in-app notification record', function () { CashAccount::factory()->create(['balance' => 1000000]); $developer = notifUserWithRole('Developer'); $this->actingAs($developer); $service = new ProductService; $product = $service->create([ 'name' => 'DB Notification Product', 'description' => 'Test', 'status' => 'active', 'category_ids' => [], 'use_same_price' => false, 'variants' => [ [ 'name' => 'Var', 'stock' => 10, 'reject_stock' => 0, 'retail_stock' => 5, 'prices' => [ ['type' => 'retail', 'price' => 50000], ], ], ], ]); $this->assertDatabaseHas('notifications', [ 'user_id' => $developer->id, 'title' => 'Produk Baru', ]); }); test('CashTransactionService deposit creates in-app notification record', function () { CashAccount::factory()->create(['balance' => 5000000]); $owner = notifUserWithRole('Owner'); $this->actingAs($owner); $service = new CashTransactionService; $service->deposit([ 'amount' => 250000, 'description' => 'Test deposit', ]); $this->assertDatabaseHas('notifications', [ 'user_id' => $owner->id, 'title' => 'Setoran Kas Toko', ]); }); test('AttendanceService checkIn creates in-app notification record', function () { $developer = notifUserWithRole('Developer'); $attData = notifEmployeeWithRole('Kasir'); $employee = $attData['employee']; $attendant = $attData['user']; $this->actingAs($attendant); $service = new AttendanceService; $service->checkIn([ 'latitude' => -6.2, 'longitude' => 106.8, ]); $this->assertDatabaseHas('notifications', [ 'user_id' => $developer->id, 'title' => 'Presensi Masuk', ]); });