2026-07-02 21:34:47 +03:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
use App\Models\SalesClientAssignment;
|
|
|
|
|
|
use App\Models\SalesPayout;
|
|
|
|
|
|
use App\Models\SalesUser;
|
|
|
|
|
|
use App\Models\Tenant;
|
|
|
|
|
|
use Carbon\Carbon;
|
|
|
|
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
|
|
|
|
use Illuminate\Support\Facades\Hash;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* TDD: SalesManagersController::store/index + SalesAccountService — заведение
|
|
|
|
|
|
* менеджеров начальником и список аккаунтов отдела (Task 7.1a, бэк).
|
|
|
|
|
|
*
|
|
|
|
|
|
* POST /api/sales/managers → head создаёт менеджера/начальника (201)
|
|
|
|
|
|
* GET /api/sales/managers → head видит список всех sales_users с метриками
|
|
|
|
|
|
*
|
|
|
|
|
|
* Оба метода — ТОЛЬКО начальник (role=head). Менеджер → 403.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Аутентификация: $this->actingAs($user, 'sales').
|
|
|
|
|
|
* Изоляция: DatabaseTransactions. Глобально подключён SharesAdminPdo (Pest.php).
|
|
|
|
|
|
* DB_DATABASE=liderra_testing ОБЯЗАТЕЛЕН при запуске.
|
|
|
|
|
|
*/
|
|
|
|
|
|
uses(DatabaseTransactions::class);
|
|
|
|
|
|
|
|
|
|
|
|
// ── helpers (уникальный префикс mgr_) ────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function mgr_head(): SalesUser
|
|
|
|
|
|
{
|
|
|
|
|
|
return SalesUser::create([
|
|
|
|
|
|
'name' => 'Начальник '.uniqid(),
|
|
|
|
|
|
'email' => 'mgrhead'.uniqid().'@sales.local',
|
|
|
|
|
|
'password' => bcrypt('secret'),
|
|
|
|
|
|
'role' => 'head',
|
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mgr_manager(): SalesUser
|
|
|
|
|
|
{
|
|
|
|
|
|
return SalesUser::create([
|
|
|
|
|
|
'name' => 'Менеджер '.uniqid(),
|
|
|
|
|
|
'email' => 'mgrmgr'.uniqid().'@sales.local',
|
|
|
|
|
|
'password' => bcrypt('secret'),
|
|
|
|
|
|
'role' => 'manager',
|
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mgr_tenant(): Tenant
|
|
|
|
|
|
{
|
|
|
|
|
|
return Tenant::factory()->create([
|
|
|
|
|
|
'balance_rub' => 0.0,
|
|
|
|
|
|
'delivered_in_month' => 0,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mgr_assignment(SalesUser $user, Tenant $tenant): SalesClientAssignment
|
|
|
|
|
|
{
|
|
|
|
|
|
return SalesClientAssignment::create([
|
|
|
|
|
|
'sales_user_id' => $user->id,
|
|
|
|
|
|
'tenant_id' => $tenant->id,
|
2026-07-05 18:51:41 +03:00
|
|
|
|
'tariff_kind' => 'topup_step',
|
|
|
|
|
|
'tariff_params' => ['threshold' => 0, 'reward' => 0, 'periods' => [['from' => 1, 'to' => 999, 'rate' => 20]]],
|
2026-07-02 21:34:47 +03:00
|
|
|
|
'assigned_at' => Carbon::now('Europe/Moscow'),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mgr_payout(int $managerId, int $headId, string $amount): SalesPayout
|
|
|
|
|
|
{
|
|
|
|
|
|
return SalesPayout::create([
|
|
|
|
|
|
'sales_user_id' => $managerId,
|
|
|
|
|
|
'amount_rub' => $amount,
|
|
|
|
|
|
'paid_on' => Carbon::now('Europe/Moscow')->format('Y-m-d'),
|
|
|
|
|
|
'comment' => null,
|
|
|
|
|
|
'created_by' => $headId,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 1. доступ: только head ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
test('менеджер POST /api/sales/managers → 403', function () {
|
|
|
|
|
|
$manager = mgr_manager();
|
|
|
|
|
|
|
|
|
|
|
|
$this->actingAs($manager, 'sales')->postJson('/api/sales/managers', [
|
|
|
|
|
|
'name' => 'Новый',
|
|
|
|
|
|
'email' => 'new'.uniqid().'@sales.local',
|
|
|
|
|
|
'password' => 'secret123',
|
|
|
|
|
|
'role' => 'manager',
|
|
|
|
|
|
])->assertStatus(403)
|
|
|
|
|
|
->assertJson(['message' => 'Доступно только начальнику отдела.']);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('менеджер GET /api/sales/managers → 403', function () {
|
|
|
|
|
|
$manager = mgr_manager();
|
|
|
|
|
|
|
|
|
|
|
|
$this->actingAs($manager, 'sales')->getJson('/api/sales/managers')
|
|
|
|
|
|
->assertStatus(403)
|
|
|
|
|
|
->assertJson(['message' => 'Доступно только начальнику отдела.']);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 2. store: happy path ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
test('head создаёт менеджера → 201, пароль захеширован, без password в ответе', function () {
|
|
|
|
|
|
$head = mgr_head();
|
|
|
|
|
|
$email = 'created'.uniqid().'@sales.local';
|
|
|
|
|
|
|
|
|
|
|
|
$response = $this->actingAs($head, 'sales')->postJson('/api/sales/managers', [
|
|
|
|
|
|
'name' => 'Иван Петров',
|
|
|
|
|
|
'email' => $email,
|
|
|
|
|
|
'password' => 'plainSecret6',
|
|
|
|
|
|
'role' => 'manager',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$response->assertCreated()
|
|
|
|
|
|
->assertJsonPath('name', 'Иван Петров')
|
|
|
|
|
|
->assertJsonPath('email', $email)
|
|
|
|
|
|
->assertJsonPath('role', 'manager')
|
|
|
|
|
|
->assertJsonPath('is_active', true);
|
|
|
|
|
|
|
|
|
|
|
|
// Ответ НЕ содержит пароль.
|
|
|
|
|
|
expect($response->json())->not->toHaveKey('password');
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('sales_users', [
|
|
|
|
|
|
'email' => $email,
|
|
|
|
|
|
'role' => 'manager',
|
|
|
|
|
|
'created_by' => $head->id,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// Пароль в БД захеширован (НЕ равен исходному) и проходит Hash::check.
|
|
|
|
|
|
$created = SalesUser::where('email', $email)->firstOrFail();
|
|
|
|
|
|
expect($created->password)->not->toBe('plainSecret6')
|
|
|
|
|
|
->and(Hash::check('plainSecret6', $created->password))->toBeTrue();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 3. store: валидация ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
test('store: дубль email → 422', function () {
|
|
|
|
|
|
$head = mgr_head();
|
|
|
|
|
|
$existing = mgr_manager();
|
|
|
|
|
|
|
|
|
|
|
|
$this->actingAs($head, 'sales')->postJson('/api/sales/managers', [
|
|
|
|
|
|
'name' => 'Дубль',
|
|
|
|
|
|
'email' => $existing->email,
|
|
|
|
|
|
'password' => 'secret123',
|
|
|
|
|
|
'role' => 'manager',
|
|
|
|
|
|
])->assertStatus(422);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('store: невалидный role → 422', function () {
|
|
|
|
|
|
$head = mgr_head();
|
|
|
|
|
|
|
|
|
|
|
|
$this->actingAs($head, 'sales')->postJson('/api/sales/managers', [
|
|
|
|
|
|
'name' => 'Кто-то',
|
|
|
|
|
|
'email' => 'role'.uniqid().'@sales.local',
|
|
|
|
|
|
'password' => 'secret123',
|
|
|
|
|
|
'role' => 'superadmin',
|
|
|
|
|
|
])->assertStatus(422);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('store: короткий пароль → 422', function () {
|
|
|
|
|
|
$head = mgr_head();
|
|
|
|
|
|
|
|
|
|
|
|
$this->actingAs($head, 'sales')->postJson('/api/sales/managers', [
|
|
|
|
|
|
'name' => 'Кто-то',
|
|
|
|
|
|
'email' => 'short'.uniqid().'@sales.local',
|
|
|
|
|
|
'password' => '123',
|
|
|
|
|
|
'role' => 'manager',
|
|
|
|
|
|
])->assertStatus(422);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 4. index: список с метриками ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
test('index (head): список с clients_count и paid_all_time_rub', function () {
|
|
|
|
|
|
$head = mgr_head();
|
|
|
|
|
|
$manager = mgr_manager();
|
|
|
|
|
|
|
|
|
|
|
|
// 2 привязки + 1 выплата у менеджера.
|
|
|
|
|
|
mgr_assignment($manager, mgr_tenant());
|
|
|
|
|
|
mgr_assignment($manager, mgr_tenant());
|
|
|
|
|
|
mgr_payout($manager->id, $head->id, '7500.00');
|
|
|
|
|
|
|
|
|
|
|
|
$response = $this->actingAs($head, 'sales')->getJson('/api/sales/managers');
|
|
|
|
|
|
|
|
|
|
|
|
$response->assertOk();
|
|
|
|
|
|
$data = collect($response->json('data'));
|
|
|
|
|
|
|
|
|
|
|
|
$row = $data->firstWhere('id', $manager->id);
|
|
|
|
|
|
expect($row)->not->toBeNull()
|
|
|
|
|
|
->and($row['clients_count'])->toBe(2)
|
|
|
|
|
|
->and((float) $row['paid_all_time_rub'])->toBe(7500.0)
|
|
|
|
|
|
->and($row)->toHaveKeys([
|
|
|
|
|
|
'id', 'name', 'email', 'role', 'is_active',
|
|
|
|
|
|
'clients_count', 'paid_all_time_rub',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// head тоже в списке (index возвращает всех sales_users).
|
|
|
|
|
|
$headRow = $data->firstWhere('id', $head->id);
|
|
|
|
|
|
expect($headRow)->not->toBeNull()
|
|
|
|
|
|
->and($headRow['role'])->toBe('head')
|
|
|
|
|
|
->and($headRow['clients_count'])->toBe(0)
|
|
|
|
|
|
->and((float) $headRow['paid_all_time_rub'])->toBe(0.0);
|
|
|
|
|
|
});
|