feat(audit): activity_log attribution — user_id/ip/ua for all 4 DealController events

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-22 17:21:53 +03:00
parent a6a7a80045
commit a9a655ee46
2 changed files with 123 additions and 4 deletions
@@ -398,10 +398,12 @@ class DealController extends Controller
$deal->comment = $validated['comment'];
ActivityLog::create([
'tenant_id' => $tenantId,
'user_id' => null,
'user_id' => request()->user()?->id,
'deal_id' => $deal->id,
'event' => 'deal.commented',
'context' => ['text' => $validated['comment'] ?? ''],
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
}
@@ -411,10 +413,12 @@ class DealController extends Controller
$deal->assigned_at = $validated['manager_id'] !== null ? now() : null;
ActivityLog::create([
'tenant_id' => $tenantId,
'user_id' => null,
'user_id' => request()->user()?->id,
'deal_id' => $deal->id,
'event' => ActivityLog::EVENT_DEAL_ASSIGNED,
'context' => ['from' => $previousManager, 'to' => $validated['manager_id']],
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
}
@@ -423,10 +427,12 @@ class DealController extends Controller
$deal->status = $validated['status'];
ActivityLog::create([
'tenant_id' => $tenantId,
'user_id' => null,
'user_id' => request()->user()?->id,
'deal_id' => $deal->id,
'event' => ActivityLog::EVENT_DEAL_STATUS_CHANGED,
'context' => ['from' => $previousStatus, 'to' => $validated['status'], 'source' => 'manual'],
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
}
@@ -534,10 +540,12 @@ class DealController extends Controller
ActivityLog::create([
'tenant_id' => $tenantId,
'user_id' => null, // на prod — request()->user()->id
'user_id' => request()->user()?->id,
'deal_id' => $deal->id,
'event' => ActivityLog::EVENT_DEAL_CREATED,
'context' => ['source' => 'manual'],
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
return $deal;
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
use App\Models\ActivityLog;
use App\Models\Deal;
use App\Models\Project;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
beforeEach(function () {
$this->tenant = Tenant::factory()->create([
'balance_leads' => 100,
]);
$this->user = User::factory()->for($this->tenant)->create();
$this->actingAs($this->user);
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$this->project = Project::factory()->for($this->tenant)->create();
});
test('ActivityLog deal.created содержит user_id, ip_address, user_agent актора', function () {
$r = $this->withServerVariables(['REMOTE_ADDR' => '10.1.2.3', 'HTTP_USER_AGENT' => 'TestBrowser/1.0'])
->postJson('/api/deals', [
'project_name' => 'Тест Attribution',
'phone' => '+7 (999) 000-11-22',
]);
$r->assertStatus(201);
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$row = ActivityLog::where('deal_id', $r->json('deal.id'))
->where('event', ActivityLog::EVENT_DEAL_CREATED)
->first();
expect($row)->not->toBeNull();
expect($row->user_id)->toBe($this->user->id);
expect($row->ip_address)->toBe('10.1.2.3');
expect($row->user_agent)->toBe('TestBrowser/1.0');
});
test('ActivityLog deal.commented содержит user_id, ip_address, user_agent актора', function () {
$deal = Deal::factory()->for($this->tenant)->for($this->project)->create(['comment' => 'old']);
$r = $this->withServerVariables(['REMOTE_ADDR' => '10.1.2.4', 'HTTP_USER_AGENT' => 'TestBrowser/2.0'])
->patchJson('/api/deals/'.$deal->id, [
'comment' => 'Новый комментарий',
]);
$r->assertStatus(200);
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$row = ActivityLog::where('deal_id', $deal->id)
->where('event', 'deal.commented')
->first();
expect($row)->not->toBeNull();
expect($row->user_id)->toBe($this->user->id);
expect($row->ip_address)->toBe('10.1.2.4');
expect($row->user_agent)->toBe('TestBrowser/2.0');
});
test('ActivityLog deal.assigned содержит user_id, ip_address, user_agent актора', function () {
$manager = User::factory()->for($this->tenant)->create(['is_active' => true]);
$deal = Deal::factory()->for($this->tenant)->for($this->project)->create([
'manager_id' => null,
'assigned_at' => null,
]);
$r = $this->withServerVariables(['REMOTE_ADDR' => '10.1.2.5', 'HTTP_USER_AGENT' => 'TestBrowser/3.0'])
->patchJson('/api/deals/'.$deal->id, [
'manager_id' => $manager->id,
]);
$r->assertStatus(200);
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$row = ActivityLog::where('deal_id', $deal->id)
->where('event', ActivityLog::EVENT_DEAL_ASSIGNED)
->first();
expect($row)->not->toBeNull();
expect($row->user_id)->toBe($this->user->id);
expect($row->ip_address)->toBe('10.1.2.5');
expect($row->user_agent)->toBe('TestBrowser/3.0');
});
test('ActivityLog deal.status_changed содержит user_id, ip_address, user_agent актора', function () {
$deal = Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'new']);
$r = $this->withServerVariables(['REMOTE_ADDR' => '10.1.2.6', 'HTTP_USER_AGENT' => 'TestBrowser/4.0'])
->patchJson('/api/deals/'.$deal->id, [
'status' => 'won',
]);
$r->assertStatus(200);
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$row = ActivityLog::where('deal_id', $deal->id)
->where('event', ActivityLog::EVENT_DEAL_STATUS_CHANGED)
->first();
expect($row)->not->toBeNull();
expect($row->user_id)->toBe($this->user->id);
expect($row->ip_address)->toBe('10.1.2.6');
expect($row->user_agent)->toBe('TestBrowser/4.0');
});