Files
portal/app/tests/Feature/Sales/SalesAttachmentApiTest.php
T
Дмитрий ba79f7d28c feat(sales): эндпоинты привязки клиентов (бэк)
Task 2.3a: SalesAttachmentController — store (менеджер подаёт заявку, 422 «уже ваш»), index (менеджер видит свои; начальник — очередь pending + история с именами менеджеров и подсказкой «свободен/уже за: …»), decide (начальник approve/reject, 403 менеджеру). Маршруты POST/GET /api/sales/attachments, POST .../{id}/decide. Тест 12/12, весь sales 100/100, stan 0. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:47:27 +03:00

274 lines
9.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\SalesAttachmentRequest;
use App\Models\SalesClientAssignment;
use App\Models\SalesUser;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Mail;
use Tests\Concerns\SharesSupplierPdo;
/**
* TDD: SalesAttachmentController — HTTP endpoints для заявок на привязку клиентов.
*
* Покрывает Task 2.3:
* - POST /api/sales/attachments → менеджер подаёт заявку
* - GET /api/sales/attachments → список (менеджер/начальник)
* - POST /api/sales/attachments/{id}/decide → решение начальника
*
* Аутентификация: Sanctum::actingAs($user, [], 'sales').
* Изоляция: DatabaseTransactions + SharesSupplierPdo (admin-db middleware).
* DB_DATABASE=liderra_testing.
*/
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
// ── helpers ──────────────────────────────────────────────────────────────────
function apiManager(array $attrs = []): SalesUser
{
return SalesUser::create(array_merge([
'name' => 'Менеджер-API '.uniqid(),
'email' => 'apimgr'.uniqid().'@sales.local',
'password' => bcrypt('secret'),
'role' => 'manager',
'is_active' => true,
], $attrs));
}
function apiHead(array $attrs = []): SalesUser
{
return SalesUser::create(array_merge([
'name' => 'Начальник-API '.uniqid(),
'email' => 'apihead'.uniqid().'@sales.local',
'password' => bcrypt('secret'),
'role' => 'head',
'is_active' => true,
], $attrs));
}
function apiTenant(string $email = ''): Tenant
{
return Tenant::factory()->create([
'contact_email' => $email !== '' ? $email : 'apiclient'.uniqid().'@example.com',
]);
}
// ── store: POST /api/sales/attachments ───────────────────────────────────────
test('manager POST /api/sales/attachments with existing client email → 201 pending', function () {
Mail::fake();
$manager = apiManager();
$tenant = apiTenant('known-client@example.com');
$response = $this->actingAs($manager, 'sales')
->postJson('/api/sales/attachments', ['login' => 'known-client@example.com']);
$response->assertCreated();
expect($response->json('status'))->toBe('pending');
expect($response->json('tenant_id'))->toBe($tenant->id);
expect($response->json('status_label'))->toBe('На рассмотрении');
});
test('manager POST with unknown login → 201 not_found', function () {
Mail::fake();
$manager = apiManager();
$response = $this->actingAs($manager, 'sales')
->postJson('/api/sales/attachments', ['login' => 'nobody-xyz@unknown.test']);
$response->assertCreated();
expect($response->json('status'))->toBe('not_found');
expect($response->json('tenant_id'))->toBeNull();
expect($response->json('status_label'))->toBe('Клиент не найден');
});
test('manager POST for client already assigned to them → 422 with plain-Russian message', function () {
Mail::fake();
$manager = apiManager();
$tenant = apiTenant('own-api-client@example.com');
SalesClientAssignment::create([
'sales_user_id' => $manager->id,
'tenant_id' => $tenant->id,
'tariff_params' => [],
'assigned_at' => now(),
]);
$response = $this->actingAs($manager, 'sales')
->postJson('/api/sales/attachments', ['login' => 'own-api-client@example.com']);
$response->assertStatus(422);
expect($response->json('message'))->toBe('Этот клиент уже ваш.');
});
test('POST without login field → 422 validation error', function () {
$manager = apiManager();
$this->actingAs($manager, 'sales')
->postJson('/api/sales/attachments', [])
->assertStatus(422);
});
// ── index: GET /api/sales/attachments ────────────────────────────────────────
test('manager GET /api/sales/attachments sees only their own requests', function () {
Mail::fake();
$managerA = apiManager();
$managerB = apiManager();
// Создаём заявки для обоих менеджеров
SalesAttachmentRequest::create([
'sales_user_id' => $managerA->id,
'login_input' => 'client-a@example.com',
'status' => 'pending',
]);
SalesAttachmentRequest::create([
'sales_user_id' => $managerB->id,
'login_input' => 'client-b@example.com',
'status' => 'pending',
]);
$response = $this->actingAs($managerA, 'sales')
->getJson('/api/sales/attachments');
$response->assertOk();
$data = $response->json('data');
expect($data)->not->toBeNull()->toHaveCount(1);
expect($data[0]['login_input'])->toBe('client-a@example.com');
});
test('head GET /api/sales/attachments sees pending queue including other managers requests', function () {
Mail::fake();
$manager = apiManager();
$head = apiHead();
// Pending заявка от менеджера
SalesAttachmentRequest::create([
'sales_user_id' => $manager->id,
'login_input' => 'head-visible@example.com',
'status' => 'pending',
]);
$response = $this->actingAs($head, 'sales')
->getJson('/api/sales/attachments');
$response->assertOk();
expect($response->json('pending'))->not->toBeNull();
expect($response->json('history'))->not->toBeNull();
$pending = $response->json('pending');
$logins = array_column($pending, 'login_input');
expect($logins)->toContain('head-visible@example.com');
});
test('head GET index returns manager_name in pending rows', function () {
Mail::fake();
$manager = apiManager(['name' => 'Иван Тестов '.uniqid()]);
$head = apiHead();
SalesAttachmentRequest::create([
'sales_user_id' => $manager->id,
'login_input' => 'head-mgr-name@example.com',
'status' => 'pending',
]);
$response = $this->actingAs($head, 'sales')
->getJson('/api/sales/attachments');
$response->assertOk();
$pending = $response->json('pending');
expect($pending)->not->toBeEmpty();
// Ищем нашу заявку
$found = collect($pending)->firstWhere('login_input', 'head-mgr-name@example.com');
expect($found)->not->toBeNull();
expect($found['manager_name'])->toContain('Иван Тестов');
});
// ── decide: POST /api/sales/attachments/{id}/decide ──────────────────────────
test('head POST decide approve → 200, assignment created', function () {
Mail::fake();
$manager = apiManager();
$head = apiHead();
$tenant = apiTenant('decide-test@example.com');
$req = SalesAttachmentRequest::create([
'sales_user_id' => $manager->id,
'login_input' => 'decide-test@example.com',
'tenant_id' => $tenant->id,
'status' => 'pending',
]);
$response = $this->actingAs($head, 'sales')
->postJson("/api/sales/attachments/{$req->id}/decide", ['action' => 'approve']);
$response->assertOk();
expect($response->json('status'))->toBe('approved');
expect($response->json('status_label'))->toBe('Одобрено');
// Spot-check: assignment создан
$assignment = SalesClientAssignment::where('tenant_id', $tenant->id)->first();
expect($assignment)->not->toBeNull();
expect($assignment->sales_user_id)->toBe($manager->id);
});
test('manager POST decide → 403 plain Russian message', function () {
Mail::fake();
$manager = apiManager();
$otherManager = apiManager();
$tenant = apiTenant('mgr-decide@example.com');
$req = SalesAttachmentRequest::create([
'sales_user_id' => $otherManager->id,
'login_input' => 'mgr-decide@example.com',
'tenant_id' => $tenant->id,
'status' => 'pending',
]);
$response = $this->actingAs($manager, 'sales')
->postJson("/api/sales/attachments/{$req->id}/decide", ['action' => 'approve']);
$response->assertStatus(403);
expect($response->json('message'))->toBe('Только начальник может решать по заявкам.');
});
test('decide with invalid action → 422 validation error', function () {
$head = apiHead();
$manager = apiManager();
$tenant = apiTenant('bad-action@example.com');
$req = SalesAttachmentRequest::create([
'sales_user_id' => $manager->id,
'login_input' => 'bad-action@example.com',
'tenant_id' => $tenant->id,
'status' => 'pending',
]);
$this->actingAs($head, 'sales')
->postJson("/api/sales/attachments/{$req->id}/decide", ['action' => 'invalidaction'])
->assertStatus(422);
});
// ── unauthenticated ───────────────────────────────────────────────────────────
test('unauthenticated POST /api/sales/attachments → 401', function () {
$this->postJson('/api/sales/attachments', ['login' => 'test@example.com'])
->assertUnauthorized();
});
test('unauthenticated GET /api/sales/attachments → 401', function () {
$this->getJson('/api/sales/attachments')
->assertUnauthorized();
});