Files
portal/app/tests/Feature/DealCreateTest.php
T

196 lines
6.7 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\ActivityLog;
use App\Models\Deal;
use App\Models\Project;
use App\Models\SupplierLeadCost;
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);
});
test('POST /api/deals создаёт сделку с manual source + project firstOrCreate', function () {
$r = $this->postJson('/api/deals', [
'project_name' => 'Окна Москва',
'phone' => '+7 (999) 123-45-67',
'contact_name' => 'Тест Тестов',
'status' => 'new',
]);
$r->assertStatus(201);
expect($r->json('deal.id'))->toBeInt();
expect($r->json('deal.tenant_id'))->toBe($this->tenant->id);
expect($r->json('deal.phone'))->toBe('+7 (999) 123-45-67');
expect($r->json('deal.status'))->toBe('new');
$dealId = $r->json('deal.id');
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$deal = Deal::query()->where('id', $dealId)->first();
expect($deal)->not->toBeNull();
expect($deal->source_crm_id)->toBeNull(); // manual
expect($deal->contact_name)->toBe('Тест Тестов');
// Project создан с type='manual'
$project = Project::find($r->json('deal.project_id'));
expect($project->name)->toBe('Окна Москва');
expect($project->type)->toBe('manual');
});
test('POST /api/deals использует существующий project (не дублирует)', function () {
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$existing = Project::create([
'tenant_id' => $this->tenant->id,
'name' => 'Натяжные потолки',
'type' => 'webhook',
]);
$r = $this->postJson('/api/deals', [
'project_name' => 'Натяжные потолки',
'phone' => '+7 (999) 000-00-00',
]);
$r->assertStatus(201);
expect($r->json('deal.project_id'))->toBe($existing->id);
// Проверяем что НЕТ нового project'а с таким же name
$count = Project::where('tenant_id', $this->tenant->id)
->where('name', 'Натяжные потолки')
->count();
expect($count)->toBe(1);
});
test('POST /api/deals пишет ActivityLog с context.source=manual', function () {
$r = $this->postJson('/api/deals', [
'project_name' => 'X',
'phone' => '+7 (999) 000-00-00',
]);
$r->assertStatus(201);
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$log = ActivityLog::where('deal_id', $r->json('deal.id'))->first();
expect($log)->not->toBeNull();
expect($log->event)->toBe(ActivityLog::EVENT_DEAL_CREATED);
expect($log->context)->toBe(['source' => 'manual']);
});
test('POST /api/deals 422 без обязательных полей', function () {
$r = $this->postJson('/api/deals', []);
$r->assertStatus(422);
expect($r->json('errors'))->toHaveKeys(['project_name', 'phone']);
});
test('POST /api/deals 401 без auth', function () {
auth()->logout();
$r = $this->postJson('/api/deals', [
'project_name' => 'X',
'phone' => '+7 (999) 000-00-00',
]);
$r->assertStatus(401);
});
test('POST /api/deals дефолтный status = new если не передан', function () {
$r = $this->postJson('/api/deals', [
'project_name' => 'X',
'phone' => '+7 (999) 000-00-00',
]);
$r->assertStatus(201);
expect($r->json('deal.status'))->toBe('new');
});
test('POST /api/deals с manager_id → assigned_at = NOW()', function () {
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$manager = User::factory()->for($this->tenant)->create(['is_active' => true]);
$r = $this->postJson('/api/deals', [
'project_name' => 'X',
'phone' => '+7 (999) 000-00-00',
'manager_id' => $manager->id,
]);
$r->assertStatus(201);
$deal = Deal::where('id', $r->json('deal.id'))->first();
expect($deal->manager_id)->toBe($manager->id);
expect($deal->assigned_at)->not->toBeNull();
});
test('POST /api/deals manual НЕ списывает баланс tenant\'а', function () {
$balanceBefore = $this->tenant->balance_leads;
$this->postJson('/api/deals', [
'project_name' => 'X',
'phone' => '+7 (999) 000-00-00',
])->assertStatus(201);
$this->tenant->refresh();
expect($this->tenant->balance_leads)->toBe($balanceBefore);
});
test('POST /api/deals manual создаёт SupplierLeadCost если у проекта есть активный supplier', function () {
// Создаём supplier + проект + project_suppliers связку.
$supplierId = DB::table('suppliers')->insertGetId([
'code' => 'test_b1_'.bin2hex(random_bytes(3)),
'name' => 'Test Supplier',
'accepts_types' => '{"websites","calls"}',
'cost_rub' => '15.00',
'channel' => 'sites',
'is_active' => true,
'sort_order' => 1,
'quality_score' => 1.00,
'created_at' => now(),
]);
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$project = Project::create([
'tenant_id' => $this->tenant->id,
'name' => 'WithSupplier',
'type' => 'manual',
]);
DB::table('project_suppliers')->insert([
'project_id' => $project->id,
'supplier_id' => $supplierId,
'is_active' => true,
'created_at' => now(),
]);
$r = $this->postJson('/api/deals', [
'project_name' => 'WithSupplier',
'phone' => '+7 (999) 000-00-00',
]);
$r->assertStatus(201);
// SupplierLeadCost создан со snapshot cost_rub
$cost = SupplierLeadCost::query()
->where('deal_id', $r->json('deal.id'))
->first();
expect($cost)->not->toBeNull();
expect($cost->supplier_id)->toBe((int) $supplierId);
expect((string) $cost->cost_rub)->toBe('15.00');
expect($cost->supplier_lead_id)->toBeNull(); // manual: нет внешнего id
});
test('POST /api/deals manual БЕЗ supplier'."'а у проекта — без SupplierLeadCost (graceful skip)", function () {
$r = $this->postJson('/api/deals', [
'project_name' => 'NoSupplier',
'phone' => '+7 (999) 000-00-00',
]);
$r->assertStatus(201);
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
$cost = SupplierLeadCost::query()
->where('deal_id', $r->json('deal.id'))
->count();
expect($cost)->toBe(0);
});