tenant = Tenant::factory()->create(); $this->otherTenant = Tenant::factory()->create(); $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('POST /api/deals/transition — 422 без обязательных полей', function () { $this->postJson('/api/deals/transition', [])->assertStatus(422); }); test('POST /api/deals/transition — 401 без auth', function () { auth()->logout(); $this->postJson('/api/deals/transition', ['ids' => [1], 'status' => 'new'])->assertStatus(401); }); 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(), 'status' => 'won', ]); $r->assertStatus(200)->assertJson([ 'updated' => 3, 'requested' => 3, 'status' => 'won', ]); foreach ($deals as $d) { $d->refresh(); expect($d->status)->toBe('won'); } $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', 'to' => 'won', 'source' => 'bulk', ]); }); test('POST /api/deals/transition — NO-OP не пишет ActivityLog', function () { $deal = Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'won']); $r = $this->postJson('/api/deals/transition', [ 'ids' => [$deal->id], 'status' => 'won', ]); $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']); // Передаём оба id — чужой не должен обновиться. $r = $this->postJson('/api/deals/transition', [ 'ids' => [$own->id, $foreign->id], 'status' => 'won', ]); $r->assertStatus(200)->assertJson([ 'updated' => 1, 'requested' => 2, ]); DB::statement('SET app.current_tenant_id = '.$this->tenant->id); $own->refresh(); expect($own->status)->toBe('won'); 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' => [], 'status' => 'won', ])->assertStatus(422); });