ba79f7d28c
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>
274 lines
9.5 KiB
PHP
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();
|
|
});
|