2026-07-02 18:56:58 +03:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
use App\Models\SalesClientAssignment;
|
|
|
|
|
|
use App\Models\SalesTariff;
|
|
|
|
|
|
use App\Models\SalesUser;
|
|
|
|
|
|
use App\Models\Tenant;
|
|
|
|
|
|
use Carbon\Carbon;
|
|
|
|
|
|
use Carbon\CarbonImmutable;
|
|
|
|
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* TDD: SalesTariffController — HTTP endpoints каталога тарифов портала продаж
|
|
|
|
|
|
* и назначения текущего тарифа менеджеру (Task 3.2a).
|
|
|
|
|
|
*
|
|
|
|
|
|
* GET /api/sales/tariffs → каталог тарифов + менеджеры (только head)
|
|
|
|
|
|
* POST /api/sales/tariffs → создать тариф (только head)
|
|
|
|
|
|
* PUT /api/sales/tariffs/{id} → обновить тариф (только head)
|
|
|
|
|
|
* POST /api/sales/tariffs/assign → назначить тариф менеджеру (только head)
|
|
|
|
|
|
*
|
|
|
|
|
|
* Аутентификация: $this->actingAs($user, 'sales').
|
|
|
|
|
|
* Изоляция: DatabaseTransactions. Глобально подключён SharesAdminPdo (Pest.php),
|
|
|
|
|
|
* даёт cross-connection visibility под middleware admin-db.
|
|
|
|
|
|
* DB_DATABASE=liderra_testing ОБЯЗАТЕЛЕН при запуске.
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ ВРЕМЯ: замораживаем на середину месяца (день 15) — детерминизм periods/tenure.
|
|
|
|
|
|
* ⚠️ ПАРТИЦИИ: balance_transactions / lead_charges партиционированы помесячно
|
|
|
|
|
|
* динамически → сеем ТОЛЬКО в текущий месяц.
|
|
|
|
|
|
*/
|
|
|
|
|
|
uses(DatabaseTransactions::class);
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(function () {
|
|
|
|
|
|
$now = CarbonImmutable::create(null, null, 15, 12, 0, 0, 'Europe/Moscow');
|
|
|
|
|
|
CarbonImmutable::setTestNow($now);
|
|
|
|
|
|
Carbon::setTestNow($now);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterEach(function () {
|
|
|
|
|
|
CarbonImmutable::setTestNow();
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── helpers (уникальный префикс tar_) ────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function tar_head(): SalesUser
|
|
|
|
|
|
{
|
|
|
|
|
|
return SalesUser::create([
|
|
|
|
|
|
'name' => 'Начальник '.uniqid(),
|
|
|
|
|
|
'email' => 'tarhead'.uniqid().'@sales.local',
|
|
|
|
|
|
'password' => bcrypt('secret'),
|
|
|
|
|
|
'role' => 'head',
|
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function tar_manager(float $baseSalary = 0.0, ?int $currentTariffId = null): SalesUser
|
|
|
|
|
|
{
|
|
|
|
|
|
return SalesUser::create([
|
|
|
|
|
|
'name' => 'Менеджер '.uniqid(),
|
|
|
|
|
|
'email' => 'tarmgr'.uniqid().'@sales.local',
|
|
|
|
|
|
'password' => bcrypt('secret'),
|
|
|
|
|
|
'role' => 'manager',
|
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
|
'base_salary_rub' => $baseSalary,
|
|
|
|
|
|
'current_tariff_id' => $currentTariffId,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function tar_tenant(): Tenant
|
|
|
|
|
|
{
|
|
|
|
|
|
return Tenant::factory()->create([
|
|
|
|
|
|
'balance_rub' => 0.0,
|
|
|
|
|
|
'delivered_in_month' => 0,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** @param array<string,mixed> $params */
|
|
|
|
|
|
function tar_tariff(string $kind, array $params, string $name = 'Тариф'): SalesTariff
|
|
|
|
|
|
{
|
|
|
|
|
|
return SalesTariff::create([
|
|
|
|
|
|
'name' => $name.' '.uniqid(),
|
|
|
|
|
|
'kind' => $kind,
|
|
|
|
|
|
'params' => $params,
|
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** @param array<string,mixed> $params */
|
|
|
|
|
|
function tar_assignment(SalesUser $user, Tenant $tenant, string $kind, array $params): SalesClientAssignment
|
|
|
|
|
|
{
|
|
|
|
|
|
return SalesClientAssignment::create([
|
|
|
|
|
|
'sales_user_id' => $user->id,
|
|
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
|
'tariff_kind' => $kind,
|
|
|
|
|
|
'tariff_params' => $params,
|
|
|
|
|
|
'assigned_at' => Carbon::now('Europe/Moscow')->subMonthsNoOverflow(2)->subDays(15),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Дата/время в ТЕКУЩЕМ месяце МСК (день $day). */
|
|
|
|
|
|
function tar_date(int $day, string $time = '10:00:00'): string
|
|
|
|
|
|
{
|
|
|
|
|
|
return CarbonImmutable::now('Europe/Moscow')->startOfMonth()
|
|
|
|
|
|
->setDay($day)->format('Y-m-d').' '.$time;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function tar_topup(int $tenantId, string $amountRub, ?string $at = null): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$dt = $at ?? tar_date(10);
|
|
|
|
|
|
DB::table('balance_transactions')->insert([
|
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
|
'type' => 'topup',
|
|
|
|
|
|
'amount_rub' => $amountRub,
|
|
|
|
|
|
'amount_leads' => 0,
|
|
|
|
|
|
'balance_rub_after' => $amountRub,
|
|
|
|
|
|
'description' => 'test',
|
|
|
|
|
|
'created_at' => $dt,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function tar_leadCharge(int $tenantId, int $kopecks, ?string $at = null): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$dt = $at ?? tar_date(10);
|
|
|
|
|
|
DB::table('lead_charges')->insert([
|
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
|
'deal_id' => fake()->numberBetween(1, 99999),
|
|
|
|
|
|
'deal_received_at' => $dt,
|
|
|
|
|
|
'tier_no' => 1,
|
|
|
|
|
|
'price_per_lead_kopecks' => $kopecks,
|
|
|
|
|
|
'charge_source' => 'rub',
|
|
|
|
|
|
'charged_at' => $dt,
|
|
|
|
|
|
'created_at' => $dt,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 1. доступ: только head ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
test('manager получает 403 на всех 4 эндпоинтах тарифов', function () {
|
|
|
|
|
|
$manager = tar_manager();
|
2026-07-05 18:51:41 +03:00
|
|
|
|
$tariff = tar_tariff('daily_salary', []);
|
2026-07-02 18:56:58 +03:00
|
|
|
|
|
|
|
|
|
|
$this->actingAs($manager, 'sales')->getJson('/api/sales/tariffs')
|
|
|
|
|
|
->assertStatus(403)
|
|
|
|
|
|
->assertJson(['message' => 'Доступно только начальнику отдела.']);
|
|
|
|
|
|
|
|
|
|
|
|
$this->actingAs($manager, 'sales')
|
2026-07-05 18:51:41 +03:00
|
|
|
|
->postJson('/api/sales/tariffs', ['name' => 'X', 'kind' => 'daily_salary', 'params' => []])
|
2026-07-02 18:56:58 +03:00
|
|
|
|
->assertStatus(403);
|
|
|
|
|
|
|
|
|
|
|
|
$this->actingAs($manager, 'sales')
|
2026-07-05 18:51:41 +03:00
|
|
|
|
->putJson("/api/sales/tariffs/{$tariff->id}", ['name' => 'X', 'kind' => 'daily_salary', 'params' => []])
|
2026-07-02 18:56:58 +03:00
|
|
|
|
->assertStatus(403);
|
|
|
|
|
|
|
|
|
|
|
|
$this->actingAs($manager, 'sales')
|
|
|
|
|
|
->postJson('/api/sales/tariffs/assign', ['manager_id' => $manager->id, 'tariff_id' => $tariff->id])
|
|
|
|
|
|
->assertStatus(403);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 2. store ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
test('head создаёт валидный topup_step тариф → 201', function () {
|
|
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
|
|
|
|
|
|
$response = $this->actingAs($head, 'sales')->postJson('/api/sales/tariffs', [
|
|
|
|
|
|
'name' => 'Ступенчатый',
|
|
|
|
|
|
'kind' => 'topup_step',
|
2026-07-05 18:51:41 +03:00
|
|
|
|
'params' => [
|
|
|
|
|
|
'threshold' => 0,
|
|
|
|
|
|
'reward' => 0,
|
|
|
|
|
|
'periods' => [
|
|
|
|
|
|
['from' => 1, 'to' => 3, 'rate' => 10],
|
|
|
|
|
|
['from' => 4, 'to' => 12, 'rate' => 5],
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
2026-07-02 18:56:58 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$response->assertCreated();
|
|
|
|
|
|
expect($response->json('kind'))->toBe('topup_step')
|
|
|
|
|
|
->and($response->json('name'))->toBe('Ступенчатый')
|
|
|
|
|
|
->and($response->json('params.periods'))->toHaveCount(2);
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('sales_tariffs', ['id' => $response->json('id'), 'kind' => 'topup_step']);
|
|
|
|
|
|
});
|
2026-07-15 15:15:10 +03:00
|
|
|
|
|
|
|
|
|
|
test('store: первая ступень from=0 (с момента привязки) → 201', function () {
|
|
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
|
|
|
|
|
|
// Дефолт формы: первая ступень «0—1» (с момента привязки клиента).
|
|
|
|
|
|
// tenure стартует с 1 → from=0 штатно ловит первый месяц. Валидация обязана пускать 0.
|
|
|
|
|
|
$this->actingAs($head, 'sales')->postJson('/api/sales/tariffs', [
|
|
|
|
|
|
'name' => 'Норма',
|
|
|
|
|
|
'kind' => 'topup_step',
|
|
|
|
|
|
'params' => [
|
|
|
|
|
|
'threshold' => 30000,
|
|
|
|
|
|
'reward' => 10000,
|
|
|
|
|
|
'periods' => [
|
|
|
|
|
|
['from' => 0, 'to' => 1, 'rate' => 10],
|
|
|
|
|
|
['from' => 1, 'to' => 2, 'rate' => 7],
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
])->assertCreated()->assertJsonPath('params.periods.0.from', 0);
|
|
|
|
|
|
});
|
2026-07-02 18:56:58 +03:00
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
test('head создаёт валидный daily_salary тариф (params {}) → 201', function () {
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
$response = $this->actingAs($head, 'sales')->postJson('/api/sales/tariffs', [
|
|
|
|
|
|
'name' => 'Оклад',
|
|
|
|
|
|
'kind' => 'daily_salary',
|
|
|
|
|
|
'params' => [],
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$response->assertCreated()
|
|
|
|
|
|
->assertJsonPath('kind', 'daily_salary')
|
|
|
|
|
|
->assertJsonPath('name', 'Оклад');
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('sales_tariffs', ['id' => $response->json('id'), 'kind' => 'daily_salary']);
|
2026-07-02 18:56:58 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
test('head создаёт валидный topup_step тариф с порогом+бонусом → 201', function () {
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
|
|
|
|
|
|
$this->actingAs($head, 'sales')->postJson('/api/sales/tariffs', [
|
2026-07-05 18:51:41 +03:00
|
|
|
|
'name' => 'Фикс-бонус',
|
|
|
|
|
|
'kind' => 'topup_step',
|
|
|
|
|
|
'params' => ['threshold' => 50000, 'reward' => 5000, 'periods' => []],
|
2026-07-02 18:56:58 +03:00
|
|
|
|
])->assertCreated()->assertJsonPath('params.reward', 5000);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
test('store: topup_step без threshold → 422', function () {
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
// threshold обязателен для topup_step — без него валидация падает.
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$this->actingAs($head, 'sales')->postJson('/api/sales/tariffs', [
|
|
|
|
|
|
'name' => 'Кривой',
|
|
|
|
|
|
'kind' => 'topup_step',
|
2026-07-05 18:51:41 +03:00
|
|
|
|
'params' => ['reward' => 0, 'periods' => []],
|
2026-07-02 18:56:58 +03:00
|
|
|
|
])->assertStatus(422);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
test('store: topup_step без reward → 422', function () {
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
// reward обязателен для topup_step — без него валидация падает.
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$this->actingAs($head, 'sales')->postJson('/api/sales/tariffs', [
|
|
|
|
|
|
'name' => 'Кривой',
|
2026-07-05 18:51:41 +03:00
|
|
|
|
'kind' => 'topup_step',
|
|
|
|
|
|
'params' => ['threshold' => 100],
|
2026-07-02 18:56:58 +03:00
|
|
|
|
])->assertStatus(422);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
test('store: kind вне списка (percent_oborot — устаревший) → 422', function () {
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
// percent_oborot выведен из обращения — правило in: отклоняет его.
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$this->actingAs($head, 'sales')->postJson('/api/sales/tariffs', [
|
2026-07-05 18:51:41 +03:00
|
|
|
|
'name' => 'Устаревший',
|
|
|
|
|
|
'kind' => 'percent_oborot',
|
|
|
|
|
|
'params' => ['rate' => 10],
|
2026-07-02 18:56:58 +03:00
|
|
|
|
])->assertStatus(422);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('store: неизвестный kind → 422', function () {
|
|
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
|
|
|
|
|
|
$this->actingAs($head, 'sales')->postJson('/api/sales/tariffs', [
|
|
|
|
|
|
'name' => 'Чужой',
|
|
|
|
|
|
'kind' => 'nonsense',
|
|
|
|
|
|
'params' => ['rate' => 1],
|
|
|
|
|
|
])->assertStatus(422);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 3. update ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
test('head обновляет name/params тарифа → 200', function () {
|
|
|
|
|
|
$head = tar_head();
|
2026-07-05 18:51:41 +03:00
|
|
|
|
$tariff = tar_tariff('topup_step', ['threshold' => 0, 'reward' => 0, 'periods' => [['from' => 1, 'to' => 999, 'rate' => 10]]]);
|
2026-07-02 18:56:58 +03:00
|
|
|
|
|
|
|
|
|
|
$response = $this->actingAs($head, 'sales')->putJson("/api/sales/tariffs/{$tariff->id}", [
|
|
|
|
|
|
'name' => 'Обновлённый',
|
2026-07-05 18:51:41 +03:00
|
|
|
|
'kind' => 'topup_step',
|
|
|
|
|
|
'params' => ['threshold' => 0, 'reward' => 0, 'periods' => [['from' => 1, 'to' => 999, 'rate' => 25]]],
|
2026-07-02 18:56:58 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$response->assertOk()
|
|
|
|
|
|
->assertJsonPath('name', 'Обновлённый')
|
2026-07-05 18:51:41 +03:00
|
|
|
|
->assertJsonPath('params.periods.0.rate', 25);
|
2026-07-02 18:56:58 +03:00
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('sales_tariffs', ['id' => $tariff->id, 'name' => 'Обновлённый']);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('update: смена kind → 422', function () {
|
|
|
|
|
|
$head = tar_head();
|
2026-07-05 18:51:41 +03:00
|
|
|
|
$tariff = tar_tariff('topup_step', ['threshold' => 0, 'reward' => 0, 'periods' => []]);
|
2026-07-02 18:56:58 +03:00
|
|
|
|
|
|
|
|
|
|
$this->actingAs($head, 'sales')->putJson("/api/sales/tariffs/{$tariff->id}", [
|
|
|
|
|
|
'name' => 'X',
|
2026-07-05 18:51:41 +03:00
|
|
|
|
'kind' => 'daily_salary',
|
|
|
|
|
|
'params' => [],
|
2026-07-02 18:56:58 +03:00
|
|
|
|
])->assertStatus(422);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('update: несуществующий тариф → 404', function () {
|
|
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
|
|
|
|
|
|
$this->actingAs($head, 'sales')->putJson('/api/sales/tariffs/999999', [
|
|
|
|
|
|
'name' => 'X',
|
2026-07-05 18:51:41 +03:00
|
|
|
|
'kind' => 'topup_step',
|
|
|
|
|
|
'params' => ['threshold' => 0, 'reward' => 0, 'periods' => []],
|
2026-07-02 18:56:58 +03:00
|
|
|
|
])->assertStatus(404);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('update НЕ трогает snapshot уже привязанных клиентов', function () {
|
|
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
$manager = tar_manager();
|
|
|
|
|
|
$tenant = tar_tenant();
|
2026-07-05 18:51:41 +03:00
|
|
|
|
$tariff = tar_tariff('topup_step', ['threshold' => 0, 'reward' => 0, 'periods' => [['from' => 1, 'to' => 999, 'rate' => 10]]]);
|
2026-07-02 18:56:58 +03:00
|
|
|
|
|
|
|
|
|
|
// snapshot клиента = rate 10 на момент привязки
|
2026-07-05 18:51:41 +03:00
|
|
|
|
$snapshotParams = ['threshold' => 0, 'reward' => 0, 'periods' => [['from' => 1, 'to' => 999, 'rate' => 10]]];
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$assignment = SalesClientAssignment::create([
|
|
|
|
|
|
'sales_user_id' => $manager->id,
|
|
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
|
'tariff_id' => $tariff->id,
|
2026-07-05 18:51:41 +03:00
|
|
|
|
'tariff_kind' => 'topup_step',
|
|
|
|
|
|
'tariff_params' => $snapshotParams,
|
2026-07-02 18:56:58 +03:00
|
|
|
|
'assigned_at' => Carbon::now('Europe/Moscow'),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$this->actingAs($head, 'sales')->putJson("/api/sales/tariffs/{$tariff->id}", [
|
|
|
|
|
|
'name' => 'Изменён',
|
2026-07-05 18:51:41 +03:00
|
|
|
|
'kind' => 'topup_step',
|
|
|
|
|
|
'params' => ['threshold' => 0, 'reward' => 0, 'periods' => [['from' => 1, 'to' => 999, 'rate' => 99]]],
|
2026-07-02 18:56:58 +03:00
|
|
|
|
])->assertOk();
|
|
|
|
|
|
|
|
|
|
|
|
$assignment->refresh();
|
2026-07-05 18:51:41 +03:00
|
|
|
|
// Снимок не тронут: ставка ступени осталась 10 (не 99), вид тарифа — topup_step.
|
|
|
|
|
|
expect((float) $assignment->tariff_params['periods'][0]['rate'])->toBe(10.0)
|
|
|
|
|
|
->and((float) $assignment->tariff_params['threshold'])->toBe(0.0)
|
|
|
|
|
|
->and($assignment->tariff_kind)->toBe('topup_step');
|
2026-07-02 18:56:58 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 4. assign ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
test('head назначает тариф + оклад менеджеру → 200 и запись в БД', function () {
|
|
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
$manager = tar_manager();
|
2026-07-05 18:51:41 +03:00
|
|
|
|
// daily_salary: ставка ₽/день задаётся per-manager (base_salary_rub).
|
|
|
|
|
|
$tariff = tar_tariff('daily_salary', []);
|
2026-07-02 18:56:58 +03:00
|
|
|
|
|
|
|
|
|
|
$response = $this->actingAs($head, 'sales')->postJson('/api/sales/tariffs/assign', [
|
|
|
|
|
|
'manager_id' => $manager->id,
|
|
|
|
|
|
'tariff_id' => $tariff->id,
|
|
|
|
|
|
'base_salary_rub' => 35000,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$response->assertOk()
|
|
|
|
|
|
->assertJsonPath('id', $manager->id)
|
|
|
|
|
|
->assertJsonPath('current_tariff_id', $tariff->id);
|
|
|
|
|
|
|
|
|
|
|
|
$manager->refresh();
|
|
|
|
|
|
expect($manager->current_tariff_id)->toBe($tariff->id)
|
|
|
|
|
|
->and((float) $manager->base_salary_rub)->toBe(35000.0);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('assign: manager_id указывает на head → 422', function () {
|
|
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
$otherHead = tar_head();
|
2026-07-05 18:51:41 +03:00
|
|
|
|
$tariff = tar_tariff('daily_salary', []);
|
2026-07-02 18:56:58 +03:00
|
|
|
|
|
|
|
|
|
|
$this->actingAs($head, 'sales')->postJson('/api/sales/tariffs/assign', [
|
|
|
|
|
|
'manager_id' => $otherHead->id,
|
|
|
|
|
|
'tariff_id' => $tariff->id,
|
|
|
|
|
|
])->assertStatus(422);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('assign НЕ меняет существующий snapshot assignment менеджера', function () {
|
|
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
$manager = tar_manager();
|
|
|
|
|
|
$tenant = tar_tenant();
|
|
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
$oldTariff = tar_tariff('topup_step', ['threshold' => 0, 'reward' => 0, 'periods' => [['from' => 1, 'to' => 999, 'rate' => 10]]]);
|
|
|
|
|
|
$newTariff = tar_tariff('daily_salary', []);
|
2026-07-02 18:56:58 +03:00
|
|
|
|
|
|
|
|
|
|
// Существующая привязка со снимком старого тарифа
|
2026-07-05 18:51:41 +03:00
|
|
|
|
$oldSnapshot = ['threshold' => 0, 'reward' => 0, 'periods' => [['from' => 1, 'to' => 999, 'rate' => 10]]];
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$assignment = SalesClientAssignment::create([
|
|
|
|
|
|
'sales_user_id' => $manager->id,
|
|
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
|
'tariff_id' => $oldTariff->id,
|
2026-07-05 18:51:41 +03:00
|
|
|
|
'tariff_kind' => 'topup_step',
|
|
|
|
|
|
'tariff_params' => $oldSnapshot,
|
2026-07-02 18:56:58 +03:00
|
|
|
|
'assigned_at' => Carbon::now('Europe/Moscow'),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// Назначаем менеджеру ДРУГОЙ текущий тариф
|
|
|
|
|
|
$this->actingAs($head, 'sales')->postJson('/api/sales/tariffs/assign', [
|
|
|
|
|
|
'manager_id' => $manager->id,
|
|
|
|
|
|
'tariff_id' => $newTariff->id,
|
|
|
|
|
|
])->assertOk();
|
|
|
|
|
|
|
|
|
|
|
|
// Снимок клиента не изменился
|
|
|
|
|
|
$assignment->refresh();
|
2026-07-05 18:51:41 +03:00
|
|
|
|
expect($assignment->tariff_kind)->toBe('topup_step')
|
|
|
|
|
|
->and((float) $assignment->tariff_params['periods'][0]['rate'])->toBe(10.0)
|
2026-07-02 18:56:58 +03:00
|
|
|
|
->and($assignment->tariff_id)->toBe($oldTariff->id);
|
|
|
|
|
|
|
|
|
|
|
|
// А current_tariff_id менеджера — обновился
|
|
|
|
|
|
$manager->refresh();
|
|
|
|
|
|
expect($manager->current_tariff_id)->toBe($newTariff->id);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 5. index ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
test('head видит каталог из 2 kind и менеджеров со всеми полями', function () {
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$head = tar_head();
|
|
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
$tStep = tar_tariff('topup_step', ['threshold' => 0, 'reward' => 0, 'periods' => [['from' => 1, 'to' => 999, 'rate' => 20]]], 'Ступень');
|
|
|
|
|
|
$tSalary = tar_tariff('daily_salary', [], 'Оклад');
|
2026-07-02 18:56:58 +03:00
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
// Менеджер с текущим тарифом topup_step (процент 20%) + клиент с пополнением.
|
|
|
|
|
|
$manager = tar_manager(baseSalary: 30000.0, currentTariffId: $tStep->id);
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$tenant = tar_tenant();
|
2026-07-05 18:51:41 +03:00
|
|
|
|
tar_assignment($manager, $tenant, 'topup_step', ['threshold' => 0, 'reward' => 0, 'periods' => [['from' => 1, 'to' => 999, 'rate' => 20]]]);
|
|
|
|
|
|
tar_topup($tenant->id, '50000.00'); // пополнения 50 000 ₽ → комиссия 20% = 10 000
|
|
|
|
|
|
tar_leadCharge($tenant->id, 5_000_000); // оборот 50 000 ₽ (для метрики oborot_rub)
|
2026-07-02 18:56:58 +03:00
|
|
|
|
|
|
|
|
|
|
$response = $this->actingAs($head, 'sales')->getJson('/api/sales/tariffs?period=this');
|
|
|
|
|
|
|
|
|
|
|
|
$response->assertOk();
|
|
|
|
|
|
|
|
|
|
|
|
$tariffs = $response->json('tariffs');
|
2026-07-05 18:51:41 +03:00
|
|
|
|
expect($tariffs)->toHaveCount(2);
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$kinds = array_column($tariffs, 'kind');
|
2026-07-05 18:51:41 +03:00
|
|
|
|
expect($kinds)->toContain('topup_step')->toContain('daily_salary')
|
|
|
|
|
|
->not->toContain('percent_oborot')->not->toContain('fix_per_client');
|
2026-07-02 18:56:58 +03:00
|
|
|
|
|
|
|
|
|
|
$managers = $response->json('managers');
|
|
|
|
|
|
$row = collect($managers)->firstWhere('id', $manager->id);
|
|
|
|
|
|
expect($row)->not->toBeNull()
|
2026-07-05 18:51:41 +03:00
|
|
|
|
->and($row['current_tariff_id'])->toBe($tStep->id)
|
2026-07-02 18:56:58 +03:00
|
|
|
|
->and($row['current_tariff_name'])->not->toBeNull()
|
|
|
|
|
|
->and($row['clients_count'])->toBe(1)
|
|
|
|
|
|
->and($row['estimated_earned_rub'])->toBeNumeric()
|
|
|
|
|
|
->and($row)->toHaveKeys([
|
|
|
|
|
|
'id', 'name', 'current_tariff_id', 'current_tariff_name',
|
|
|
|
|
|
'base_salary_rub', 'clients_count', 'topups_rub', 'oborot_rub', 'estimated_earned_rub',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
// topup_step: оклад НЕ начисляется. estimated = процент 20% от пополнений 50 000 = 10 000.
|
2026-07-02 18:56:58 +03:00
|
|
|
|
// JSON целое число декодируется как int → сравниваем по значению.
|
2026-07-05 18:51:41 +03:00
|
|
|
|
expect((float) $row['estimated_earned_rub'])->toBe(10000.0);
|
2026-07-02 18:56:58 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-05 18:51:41 +03:00
|
|
|
|
test('index: менеджер без текущего тарифа → estimated_earned_rub = 0', function () {
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$head = tar_head();
|
2026-07-05 18:51:41 +03:00
|
|
|
|
// Оклад задан, но без текущего тарифа он не начисляется (оклад только у daily_salary).
|
2026-07-02 18:56:58 +03:00
|
|
|
|
$manager = tar_manager(baseSalary: 25000.0, currentTariffId: null);
|
|
|
|
|
|
|
|
|
|
|
|
$response = $this->actingAs($head, 'sales')->getJson('/api/sales/tariffs');
|
|
|
|
|
|
|
|
|
|
|
|
$response->assertOk();
|
|
|
|
|
|
$row = collect($response->json('managers'))->firstWhere('id', $manager->id);
|
|
|
|
|
|
expect($row)->not->toBeNull()
|
2026-07-05 18:51:41 +03:00
|
|
|
|
->and((float) $row['estimated_earned_rub'])->toBe(0.0)
|
2026-07-02 18:56:58 +03:00
|
|
|
|
->and($row['current_tariff_id'])->toBeNull();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 6. unauthenticated ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
test('неаутентифицированный GET /api/sales/tariffs → 401', function () {
|
|
|
|
|
|
$this->getJson('/api/sales/tariffs')->assertUnauthorized();
|
|
|
|
|
|
});
|