612bf71928
Клиент не должен видеть внутреннюю схему каналов поставщика (B1_/B2_/B6_…). Фронт срезал префикс только на экране, но API сделок, публичный API тенанта и экспорт CSV/XLSX отдавали сырое имя проекта — префикс утекал клиенту. - new App\Support\SupplierProjectName::strip() (regex ^B\d+_) — серверный срез - применён в DealController (SPA), V1\DealsController (публичный API), DealExportController (экспорт) - фронтовый stripChannelPrefix расширен ^B[123]_ -> ^B\d+_ (закрывает B6/B8) - убрано имя поставщика из комментариев клиентского фронта; admin-строки -> crm.lead.store - phpstan-baseline перегенерён (сдвиг счётчиков Pest-ложняков + убран устаревший ignore) - тесты: unit 6 + feature x3 (RED->GREEN), Pest 68/68, Vitest 9/9, Pint clean, stan 0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
117 lines
5.0 KiB
PHP
117 lines
5.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
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;
|
|
|
|
/**
|
|
* Тесты POST /api/deals/export — экспорт по диапазону дат поставки.
|
|
*
|
|
* Редизайн «Сделки» (2026-05-17, Task A5): вместо ids[] — received_from/received_to.
|
|
* Конвенции: DatabaseTransactions + actingAs + SET app.current_tenant_id
|
|
* (аналогично DealIndexTest.php).
|
|
*/
|
|
uses(DatabaseTransactions::class);
|
|
|
|
beforeEach(function () {
|
|
$this->tenant = 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(['name' => 'Окна Москва']);
|
|
});
|
|
|
|
test('POST /api/deals/export требует auth', function () {
|
|
auth()->logout();
|
|
$this->postJson('/api/deals/export', ['format' => 'csv'])->assertStatus(401);
|
|
});
|
|
|
|
test('POST /api/deals/export csv возвращает сделки в диапазоне дат', function () {
|
|
Deal::factory()->for($this->tenant)->for($this->project)->create([
|
|
'phone' => '+7 999 111-11-11', 'received_at' => '2026-05-15 10:00:00',
|
|
]);
|
|
Deal::factory()->for($this->tenant)->for($this->project)->create([
|
|
'phone' => '+7 999 222-22-22', 'received_at' => '2026-05-25 10:00:00',
|
|
]);
|
|
|
|
$r = $this->post('/api/deals/export', [
|
|
'received_from' => '2026-05-14', 'received_to' => '2026-05-16', 'format' => 'csv',
|
|
]);
|
|
|
|
$r->assertStatus(200);
|
|
$r->assertHeader('content-type', 'text/csv; charset=utf-8');
|
|
$body = $r->streamedContent();
|
|
expect($body)->toContain('+7 999 111-11-11');
|
|
expect($body)->not->toContain('+7 999 222-22-22');
|
|
});
|
|
|
|
test('POST /api/deals/export xlsx отдаёт spreadsheet content-type', function () {
|
|
Deal::factory()->for($this->tenant)->for($this->project)->create(['received_at' => '2026-05-15 10:00:00']);
|
|
|
|
$r = $this->post('/api/deals/export', ['format' => 'xlsx']);
|
|
|
|
$r->assertStatus(200);
|
|
$r->assertHeader('content-type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
});
|
|
|
|
test('POST /api/deals/export не экспортирует чужой tenant (RLS)', function () {
|
|
$other = Tenant::factory()->create();
|
|
DB::statement('SET app.current_tenant_id = '.$other->id);
|
|
$foreignProject = Project::factory()->for($other)->create();
|
|
Deal::factory()->for($other)->for($foreignProject)->create(['phone' => '+7 900 000-00-00']);
|
|
|
|
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
|
$r = $this->post('/api/deals/export', ['format' => 'csv']);
|
|
|
|
expect($r->streamedContent())->not->toContain('+7 900 000-00-00');
|
|
});
|
|
|
|
test('POST /api/deals/export 422 на неизвестный format', function () {
|
|
$this->postJson('/api/deals/export', ['format' => 'pdf'])->assertStatus(422);
|
|
});
|
|
|
|
test('POST /api/deals/export без format по умолчанию отдаёт CSV', function () {
|
|
Deal::factory()->for($this->tenant)->for($this->project)->create(['received_at' => '2026-05-15 10:00:00']);
|
|
$r = $this->post('/api/deals/export', []);
|
|
$r->assertStatus(200);
|
|
$r->assertHeader('content-type', 'text/csv; charset=utf-8');
|
|
});
|
|
|
|
test('POST /api/deals/export нейтрализует CSV-формулы в свободном тексте (F-CSV)', function () {
|
|
Deal::factory()->for($this->tenant)->for($this->project)->create([
|
|
'received_at' => '2026-05-15 10:00:00',
|
|
'comment' => '=HYPERLINK("http://evil","нажми")',
|
|
'city' => '@SUM(1+1)',
|
|
]);
|
|
|
|
$r = $this->post('/api/deals/export', ['format' => 'csv']);
|
|
|
|
$body = $r->streamedContent();
|
|
// Формула нейтрализована префиксом-апострофом, исходная формула в начале
|
|
// ячейки отсутствует.
|
|
expect($body)->toContain("'=HYPERLINK(");
|
|
expect($body)->toContain("'@SUM(1+1)");
|
|
// До фикса ячейка-комментарий обрамлялась как "=HYPERLINK(...; city — как ;@SUM.
|
|
expect($body)->not->toContain('"=HYPERLINK(');
|
|
expect($body)->not->toContain(';@SUM');
|
|
});
|
|
|
|
test('POST /api/deals/export срезает канальный префикс B<N>_ из источника (не палим поставщика)', function () {
|
|
$bgProject = Project::factory()->for($this->tenant)->create(['name' => 'B6_okna.ru [12]']);
|
|
Deal::factory()->for($this->tenant)->for($bgProject)->create([
|
|
'phone' => '+7 999 333-33-33', 'received_at' => '2026-05-15 10:00:00',
|
|
]);
|
|
|
|
$body = $this->post('/api/deals/export', [
|
|
'received_from' => '2026-05-14', 'received_to' => '2026-05-16', 'format' => 'csv',
|
|
])->streamedContent();
|
|
|
|
expect($body)->toContain('okna.ru [12]');
|
|
expect($body)->not->toContain('B6_');
|
|
});
|