mock(GraphClientInterface::class, function (MockInterface $mock) { $mock->shouldReceive('listPolicies') ->atLeast() ->once() ->andReturn(new GraphResponse(true, [], 200)); }); $sync = app(InventorySyncService::class); $selectionPayload = $sync->defaultSelectionPayload(); $computed = $sync->normalizeAndHashSelection($selectionPayload); $policyTypes = $computed['selection']['policy_types']; $run = $sync->createPendingRunForUser($tenant, $user, $computed['selection']); /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $opRun = $opService->ensureRun( tenant: $tenant, type: 'inventory.sync', inputs: $computed['selection'], initiator: $user, ); $job = new RunInventorySyncJob( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), inventorySyncRunId: (int) $run->getKey(), operationRun: $opRun, ); $job->handle($sync, app(AuditLogger::class), $opService); $run->refresh(); $opRun->refresh(); expect($run->user_id)->toBe($user->id); expect($run->status)->toBe(InventorySyncRun::STATUS_SUCCESS); expect($run->started_at)->not->toBeNull(); expect($run->finished_at)->not->toBeNull(); expect($opRun->status)->toBe('completed'); expect($opRun->outcome)->toBe('succeeded'); $counts = is_array($opRun->summary_counts) ? $opRun->summary_counts : []; expect((int) ($counts['total'] ?? 0))->toBe(count($policyTypes)); expect((int) ($counts['processed'] ?? 0))->toBe(count($policyTypes)); expect((int) ($counts['succeeded'] ?? 0))->toBe(count($policyTypes)); expect((int) ($counts['failed'] ?? 0))->toBe(0); expect($user->notifications()->count())->toBe(1); $this->assertDatabaseHas('notifications', [ 'notifiable_id' => $user->getKey(), 'notifiable_type' => $user->getMorphClass(), 'type' => OperationRunCompleted::class, 'data->title' => 'Inventory sync completed', ]); }); it('maps skipped inventory sync runs to bulk progress as skipped with reason', function () { [$user, $tenant] = createUserWithTenant(role: 'owner'); $sync = app(InventorySyncService::class); $selectionPayload = $sync->defaultSelectionPayload(); $run = $sync->createPendingRunForUser($tenant, $user, $selectionPayload); $computed = $sync->normalizeAndHashSelection($selectionPayload); $policyTypes = $computed['selection']['policy_types']; $run->update(['selection_payload' => $computed['selection']]); /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $opRun = $opService->ensureRun( tenant: $tenant, type: 'inventory.sync', inputs: $computed['selection'], initiator: $user, ); $mockSync = \Mockery::mock(InventorySyncService::class); $mockSync ->shouldReceive('executePendingRun') ->once() ->andReturnUsing(function (InventorySyncRun $inventorySyncRun) { $inventorySyncRun->forceFill([ 'status' => InventorySyncRun::STATUS_SKIPPED, 'error_codes' => ['locked'], 'selection_payload' => $inventorySyncRun->selection_payload ?? [], 'started_at' => now(), 'finished_at' => now(), ])->save(); return $inventorySyncRun; }); $job = new RunInventorySyncJob( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), inventorySyncRunId: (int) $run->getKey(), operationRun: $opRun, ); $job->handle($mockSync, app(AuditLogger::class), $opService); $run->refresh(); $opRun->refresh(); expect($run->status)->toBe(InventorySyncRun::STATUS_SKIPPED); expect($opRun->status)->toBe('completed'); expect($opRun->outcome)->toBe('failed'); $counts = is_array($opRun->summary_counts) ? $opRun->summary_counts : []; expect((int) ($counts['processed'] ?? 0))->toBe(count($policyTypes)); expect((int) ($counts['skipped'] ?? 0))->toBe(count($policyTypes)); expect((int) ($counts['succeeded'] ?? 0))->toBe(0); expect((int) ($counts['failed'] ?? 0))->toBe(0); $failures = is_array($opRun->failure_summary) ? $opRun->failure_summary : []; expect($failures)->toBeArray(); expect($failures[0]['code'] ?? null)->toBe('inventory.skipped'); expect($failures[0]['message'] ?? null)->toBe('locked'); expect($user->notifications()->count())->toBe(1); $this->assertDatabaseHas('notifications', [ 'notifiable_id' => $user->getKey(), 'notifiable_type' => $user->getMorphClass(), 'type' => OperationRunCompleted::class, 'data->title' => 'Inventory sync failed', ]); });