feat(audit): ProjectService writes tenant_operations_log on create/update/delete/bulk

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-22 18:10:30 +03:00
parent bf47d46a8e
commit ed8a971b6c
2 changed files with 163 additions and 1 deletions
+64 -1
View File
@@ -9,12 +9,17 @@ use App\Jobs\SyncSupplierProjectJob;
use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Services\Audit\OperationsLogger;
use App\Services\Supplier\SupplierProjectGrouping;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\DB;
class ProjectService
{
public function __construct(
private readonly OperationsLogger $ops = new OperationsLogger(),
) {}
public function update(Project $project, array $data): Project
{
// Immutable fields — silently drop (don't 422)
@@ -63,6 +68,9 @@ class ProjectService
|| array_key_exists('sms_keyword', $data);
$oldAgnostic = $identifierFieldsTouched ? SupplierProjectGrouping::buildUniqueKeyAgnostic($project) : null;
// Snapshot before mutation for audit diff
$before = array_intersect_key($project->toArray(), $data);
$project->update($data);
if ($oldAgnostic !== null) {
@@ -72,6 +80,18 @@ class ProjectService
}
}
$this->ops->record(
tenantId: $project->tenant_id,
userId: auth()->id(),
entityType: 'project',
entityId: $project->id,
event: 'project.updated',
payloadBefore: $before,
payloadAfter: $data,
ip: request()->ip(),
userAgent: request()->userAgent(),
);
if ($needsResync) {
SyncSupplierProjectJob::dispatch($project->id);
}
@@ -142,8 +162,25 @@ class ProjectService
->pluck('supplier_project_id')
->all();
// Snapshot ДО удаления — после delete() модель недоступна.
$tenantId = $project->tenant_id;
$projectId = $project->id;
$snapshot = $project->toArray();
$project->delete(); // hard delete (Project без SoftDeletes); cascade чистит pivot + служебные.
$this->ops->record(
tenantId: $tenantId,
userId: auth()->id(),
entityType: 'project',
entityId: $projectId,
event: 'project.deleted',
payloadBefore: $snapshot,
payloadAfter: null,
ip: request()->ip(),
userAgent: request()->userAgent(),
);
if ($supplierProjectIds !== []) {
DeleteSupplierProjectJob::dispatch(array_map('intval', $supplierProjectIds));
}
@@ -190,7 +227,7 @@ class ProjectService
$query = Project::where('tenant_id', $tenantId)->whereIn('id', $ids);
return match ($action) {
$result = match ($action) {
'pause' => $this->bulkPauseResume($query, false),
'resume' => $this->bulkPauseResume($query, true),
'delete' => $this->bulkDelete($query),
@@ -198,6 +235,20 @@ class ProjectService
'update_days' => $this->bulkUpdateDays($query, $payload),
'update_limit' => $this->bulkUpdateLimit($query, $payload),
};
$this->ops->record(
tenantId: $tenantId,
userId: auth()->id(),
entityType: 'project',
entityId: null,
event: 'project.bulk_' . $action,
payloadBefore: null,
payloadAfter: array_merge(['ids' => $ids], $result),
ip: request()->ip(),
userAgent: request()->userAgent(),
);
return $result;
}
/**
@@ -412,6 +463,18 @@ class ProjectService
$project = Project::create($data);
$this->ops->record(
tenantId: $project->tenant_id,
userId: auth()->id(),
entityType: 'project',
entityId: $project->id,
event: 'project.created',
payloadBefore: null,
payloadAfter: $project->toArray(),
ip: request()->ip(),
userAgent: request()->userAgent(),
);
SyncSupplierProjectJob::dispatch($project->id);
return $project->fresh();