Files
portal/app/tests/Feature/Sales/SalesManagerAccountTest.php
T
Дмитрий daa41bcecf feat(sales): пересмотр состава тарифов портала продаж — 2 вида
Решение заказчика 03.07.2026: два вида тарифа вместо трёх — суточный
оклад daily_salary и процент от пополнений topup_step. Убраны
percent_oborot и fix_per_client.

Миграция 2026_07_03_120000 меняет CHECK sales_tariffs.kind, освобождает
FK sales_users.current_tariff_id и sales_client_assignments.tariff_id,
удаляет тарифы уходящих видов. SalesEarningsService для уходящих видов
возвращает 0 по default-ветке. Снимки привязок не трогаются.

Обновлены контроллеры тарифов и дохода, сервисы Earnings и Metrics,
фронт SalesTariffsView и api/sales.ts, демо-сид, тесты бэка и фронта,
CHANGELOG схемы v8.61 и спека портала.

Не на проде: ветка feat/sales-portal-demo.

Проверка: Sales Feature 169/169, Vitest SalesTariffs 10/10, Larastan 0.

Co-Authored-By: Claude Opus 4.8 1M context <noreply@anthropic.com>
2026-07-05 19:25:19 +03:00

206 lines
7.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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,
'tariff_kind' => 'topup_step',
'tariff_params' => ['threshold' => 0, 'reward' => 0, 'periods' => [['from' => 1, 'to' => 999, 'rate' => 20]]],
'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);
});