TenantAtlas/app/Jobs/RunInventorySyncJob.php

301 lines
11 KiB
PHP

<?php
namespace App\Jobs;
use App\Jobs\Middleware\TrackOperationRun;
use App\Models\BulkOperationRun;
use App\Models\InventorySyncRun;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Models\User;
use App\Services\BulkOperationService;
use App\Services\Intune\AuditLogger;
use App\Services\Inventory\InventorySyncService;
use App\Services\OperationRunService;
use App\Support\OperationRunLinks;
use Filament\Notifications\Notification;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use RuntimeException;
class RunInventorySyncJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public ?OperationRun $operationRun = null;
/**
* Create a new job instance.
*/
public function __construct(
public int $tenantId,
public int $userId,
public int $bulkRunId,
public int $inventorySyncRunId,
?OperationRun $operationRun = null
) {
$this->operationRun = $operationRun;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new TrackOperationRun];
}
/**
* Execute the job.
*/
public function handle(BulkOperationService $bulkOperationService, InventorySyncService $inventorySyncService, AuditLogger $auditLogger): void
{
$tenant = Tenant::query()->find($this->tenantId);
if (! $tenant instanceof Tenant) {
throw new RuntimeException('Tenant not found.');
}
$user = User::query()->find($this->userId);
if (! $user instanceof User) {
throw new RuntimeException('User not found.');
}
$bulkRun = BulkOperationRun::query()->find($this->bulkRunId);
if (! $bulkRun instanceof BulkOperationRun) {
throw new RuntimeException('BulkOperationRun not found.');
}
$run = InventorySyncRun::query()->find($this->inventorySyncRunId);
if (! $run instanceof InventorySyncRun) {
throw new RuntimeException('InventorySyncRun not found.');
}
$bulkOperationService->start($bulkRun);
$processedPolicyTypes = [];
// Note: The TrackOperationRun middleware will automatically set status to 'running' at start.
// It will also handle success completion if no exceptions thrown.
// However, InventorySyncService execution logic might be complex with partial failures.
// We might want to explicitly update the OperationRun if partial failures occur.
$run = $inventorySyncService->executePendingRun(
$run,
$tenant,
function (string $policyType, bool $success, ?string $errorCode) use ($bulkOperationService, $bulkRun, &$processedPolicyTypes): void {
$processedPolicyTypes[] = $policyType;
if ($success) {
$bulkOperationService->recordSuccess($bulkRun);
return;
}
$bulkOperationService->recordFailure($bulkRun, $policyType, $errorCode ?? 'failed');
},
);
$policyTypes = is_array($bulkRun->item_ids ?? null) ? $bulkRun->item_ids : [];
if ($policyTypes === []) {
$policyTypes = is_array($run->selection_payload['policy_types'] ?? null) ? $run->selection_payload['policy_types'] : [];
}
// --- Helper to update OperationRun with rich context ---
$updateOpRun = function (string $outcome, array $counts = [], array $failures = []) {
if ($this->operationRun) {
/** @var OperationRunService $opService */
$opService = app(OperationRunService::class);
$opService->updateRun(
$this->operationRun,
'completed',
$outcome,
$counts,
$failures
);
}
};
// -----------------------------------------------------
if ($run->status === InventorySyncRun::STATUS_SUCCESS) {
$bulkOperationService->complete($bulkRun);
// Update Operation Run explicitly to provide counts
$updateOpRun('succeeded', [
'observed' => $run->items_observed_count,
'upserted' => $run->items_upserted_count,
]);
$auditLogger->log(
tenant: $tenant,
action: 'inventory.sync.completed',
context: [
'metadata' => [
'inventory_sync_run_id' => $run->id,
'bulk_run_id' => $bulkRun->id,
'selection_hash' => $run->selection_hash,
'observed' => $run->items_observed_count,
'upserted' => $run->items_upserted_count,
],
],
actorId: $user->id,
actorEmail: $user->email,
actorName: $user->name,
resourceType: 'inventory_sync_run',
resourceId: (string) $run->id,
);
Notification::make()
->title('Inventory sync completed')
->body('Inventory sync finished successfully.')
->success()
->actions($this->operationRun ? [
\Filament\Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($this->operationRun, $tenant)),
] : [])
->sendToDatabase($user)
->send();
return;
}
if ($run->status === InventorySyncRun::STATUS_PARTIAL) {
$bulkOperationService->complete($bulkRun);
$updateOpRun('partially_succeeded', [
'observed' => $run->items_observed_count,
'upserted' => $run->items_upserted_count,
'errors' => $run->errors_count,
], [
// Minimal error summary
['code' => 'PARTIAL_SYNC', 'message' => "Errors: {$run->errors_count}"],
]);
$auditLogger->log(
tenant: $tenant,
action: 'inventory.sync.partial',
context: [
'metadata' => [
'inventory_sync_run_id' => $run->id,
'bulk_run_id' => $bulkRun->id,
'selection_hash' => $run->selection_hash,
'observed' => $run->items_observed_count,
'upserted' => $run->items_upserted_count,
'errors' => $run->errors_count,
],
],
actorId: $user->id,
actorEmail: $user->email,
actorName: $user->name,
status: 'failure',
resourceType: 'inventory_sync_run',
resourceId: (string) $run->id,
);
Notification::make()
->title('Inventory sync completed with errors')
->body('Inventory sync finished with some errors. Review the run details for error codes.')
->warning()
->actions($this->operationRun ? [
\Filament\Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($this->operationRun, $tenant)),
] : [])
->sendToDatabase($user)
->send();
return;
}
if ($run->status === InventorySyncRun::STATUS_SKIPPED) {
$reason = (string) (($run->error_codes ?? [])[0] ?? 'skipped');
foreach ($policyTypes as $policyType) {
$bulkOperationService->recordSkippedWithReason($bulkRun, (string) $policyType, $reason);
}
$bulkOperationService->complete($bulkRun);
$updateOpRun('failed', [], [['code' => 'SKIPPED', 'message' => $reason]]);
$auditLogger->log(
tenant: $tenant,
action: 'inventory.sync.skipped',
context: [
'metadata' => [
'inventory_sync_run_id' => $run->id,
'bulk_run_id' => $bulkRun->id,
'selection_hash' => $run->selection_hash,
'reason' => $reason,
],
],
actorId: $user->id,
actorEmail: $user->email,
actorName: $user->name,
resourceType: 'inventory_sync_run',
resourceId: (string) $run->id,
);
Notification::make()
->title('Inventory sync skipped')
->body('Inventory sync could not start due to locks or concurrency limits.')
->warning()
->actions($this->operationRun ? [
\Filament\Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($this->operationRun, $tenant)),
] : [])
->sendToDatabase($user)
->send();
return;
}
$reason = (string) (($run->error_codes ?? [])[0] ?? 'failed');
$missingPolicyTypes = array_values(array_diff($policyTypes, array_unique($processedPolicyTypes)));
foreach ($missingPolicyTypes as $policyType) {
$bulkOperationService->recordFailure($bulkRun, (string) $policyType, $reason);
}
$bulkOperationService->complete($bulkRun);
$updateOpRun('failed', [], [['code' => 'FAILED', 'message' => $reason]]);
$auditLogger->log(
tenant: $tenant,
action: 'inventory.sync.failed',
context: [
'metadata' => [
'inventory_sync_run_id' => $run->id,
'bulk_run_id' => $bulkRun->id,
'selection_hash' => $run->selection_hash,
'reason' => $reason,
],
],
actorId: $user->id,
actorEmail: $user->email,
actorName: $user->name,
status: 'failure',
resourceType: 'inventory_sync_run',
resourceId: (string) $run->id,
);
Notification::make()
->title('Inventory sync failed')
->body('Inventory sync finished with errors.')
->danger()
->actions($this->operationRun ? [
\Filament\Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($this->operationRun, $tenant)),
] : [])
->sendToDatabase($user)
->send();
}
}