2026-05-09 07:46:19 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
use App\Models\ActivityLog;
|
|
|
|
|
use App\Models\Deal;
|
|
|
|
|
use App\Models\Project;
|
|
|
|
|
use App\Models\Tenant;
|
2026-05-16 15:14:17 +03:00
|
|
|
use App\Models\User;
|
2026-05-09 07:46:19 +03:00
|
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Тесты POST /api/deals/transition — bulk status-update для DealsView bulk-actions.
|
|
|
|
|
*
|
|
|
|
|
* Покрывает: validation (422 на missing/неизвестный slug), RLS+app-фильтр
|
|
|
|
|
* (чужие сделки НЕ обновляются), ActivityLog event=deal.status_changed,
|
2026-05-16 15:14:17 +03:00
|
|
|
* 401 без auth, NO-OP не пишет audit entry, partial update (несколько id
|
2026-05-09 07:46:19 +03:00
|
|
|
* принадлежат tenant'у, один — нет → updated < requested).
|
|
|
|
|
*/
|
|
|
|
|
uses(DatabaseTransactions::class);
|
|
|
|
|
|
|
|
|
|
beforeEach(function () {
|
|
|
|
|
$this->tenant = Tenant::factory()->create();
|
|
|
|
|
$this->otherTenant = Tenant::factory()->create();
|
|
|
|
|
|
2026-05-16 15:14:17 +03:00
|
|
|
$this->user = User::factory()->for($this->tenant)->create();
|
|
|
|
|
$this->actingAs($this->user);
|
|
|
|
|
|
2026-05-09 07:46:19 +03:00
|
|
|
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
|
|
|
|
$this->project = Project::factory()->for($this->tenant)->create();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('POST /api/deals/transition — 422 без обязательных полей', function () {
|
|
|
|
|
$this->postJson('/api/deals/transition', [])->assertStatus(422);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-16 15:14:17 +03:00
|
|
|
test('POST /api/deals/transition — 401 без auth', function () {
|
|
|
|
|
auth()->logout();
|
|
|
|
|
$this->postJson('/api/deals/transition', ['ids' => [1], 'status' => 'new'])->assertStatus(401);
|
2026-05-09 07:46:19 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('POST /api/deals/transition — 422 на неизвестный status slug', function () {
|
|
|
|
|
$deal = Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'new']);
|
|
|
|
|
|
|
|
|
|
$r = $this->postJson('/api/deals/transition', [
|
|
|
|
|
'ids' => [$deal->id],
|
|
|
|
|
'status' => 'not_a_real_slug',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$r->assertStatus(422);
|
|
|
|
|
expect($r->json('errors.status.0'))->toContain('lead_statuses');
|
|
|
|
|
|
|
|
|
|
// Сделка осталась в исходном статусе.
|
|
|
|
|
$deal->refresh();
|
|
|
|
|
expect($deal->status)->toBe('new');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('POST /api/deals/transition — обновляет статус и пишет ActivityLog', function () {
|
|
|
|
|
$deals = Deal::factory()->count(3)->for($this->tenant)->for($this->project)->create(['status' => 'new']);
|
|
|
|
|
|
|
|
|
|
$r = $this->postJson('/api/deals/transition', [
|
|
|
|
|
'ids' => $deals->pluck('id')->all(),
|
2026-05-17 18:18:00 +03:00
|
|
|
'status' => 'won',
|
2026-05-09 07:46:19 +03:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$r->assertStatus(200)->assertJson([
|
|
|
|
|
'updated' => 3,
|
|
|
|
|
'requested' => 3,
|
2026-05-17 18:18:00 +03:00
|
|
|
'status' => 'won',
|
2026-05-09 07:46:19 +03:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
foreach ($deals as $d) {
|
|
|
|
|
$d->refresh();
|
2026-05-17 18:18:00 +03:00
|
|
|
expect($d->status)->toBe('won');
|
2026-05-09 07:46:19 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$activity = ActivityLog::where('tenant_id', $this->tenant->id)
|
|
|
|
|
->where('event', ActivityLog::EVENT_DEAL_STATUS_CHANGED)
|
|
|
|
|
->get();
|
|
|
|
|
expect($activity)->toHaveCount(3);
|
|
|
|
|
expect($activity->first()->context)->toMatchArray([
|
|
|
|
|
'from' => 'new',
|
2026-05-17 18:18:00 +03:00
|
|
|
'to' => 'won',
|
2026-05-09 07:46:19 +03:00
|
|
|
'source' => 'bulk',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('POST /api/deals/transition — NO-OP не пишет ActivityLog', function () {
|
2026-05-17 18:18:00 +03:00
|
|
|
$deal = Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'won']);
|
2026-05-09 07:46:19 +03:00
|
|
|
|
|
|
|
|
$r = $this->postJson('/api/deals/transition', [
|
|
|
|
|
'ids' => [$deal->id],
|
2026-05-17 18:18:00 +03:00
|
|
|
'status' => 'won',
|
2026-05-09 07:46:19 +03:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$r->assertStatus(200)->assertJson(['updated' => 0, 'requested' => 1]);
|
|
|
|
|
|
|
|
|
|
expect(ActivityLog::where('deal_id', $deal->id)
|
|
|
|
|
->where('event', ActivityLog::EVENT_DEAL_STATUS_CHANGED)
|
|
|
|
|
->count())->toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('POST /api/deals/transition — defense-in-depth не апдейтит чужие сделки', function () {
|
|
|
|
|
$own = Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'new']);
|
|
|
|
|
|
|
|
|
|
DB::statement('SET app.current_tenant_id = '.$this->otherTenant->id);
|
|
|
|
|
$foreignProject = Project::factory()->for($this->otherTenant)->create();
|
|
|
|
|
$foreign = Deal::factory()->for($this->otherTenant)->for($foreignProject)->create(['status' => 'new']);
|
|
|
|
|
|
2026-05-16 15:14:17 +03:00
|
|
|
// Передаём оба id — чужой не должен обновиться.
|
2026-05-09 07:46:19 +03:00
|
|
|
$r = $this->postJson('/api/deals/transition', [
|
|
|
|
|
'ids' => [$own->id, $foreign->id],
|
2026-05-17 18:18:00 +03:00
|
|
|
'status' => 'won',
|
2026-05-09 07:46:19 +03:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$r->assertStatus(200)->assertJson([
|
|
|
|
|
'updated' => 1,
|
|
|
|
|
'requested' => 2,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
|
|
|
|
$own->refresh();
|
2026-05-17 18:18:00 +03:00
|
|
|
expect($own->status)->toBe('won');
|
2026-05-09 07:46:19 +03:00
|
|
|
|
|
|
|
|
DB::statement('SET app.current_tenant_id = '.$this->otherTenant->id);
|
|
|
|
|
$foreign->refresh();
|
|
|
|
|
expect($foreign->status)->toBe('new');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('POST /api/deals/transition — 422 если ids пустой массив', function () {
|
|
|
|
|
$this->postJson('/api/deals/transition', [
|
|
|
|
|
'ids' => [],
|
2026-05-17 18:18:00 +03:00
|
|
|
'status' => 'won',
|
2026-05-09 07:46:19 +03:00
|
|
|
])->assertStatus(422);
|
|
|
|
|
});
|