Files
portal/app/tests/Feature/Sales/SalesAttachmentDecideTest.php
T
Дмитрий f7c9721058 feat(sales): решение начальника по заявке + снимок тарифа
Task 2.2: SalesAttachmentService.decide(head, requestId, approve|reject) — только начальник (иначе AuthorizationException). approve: создаёт sales_client_assignment со СНИМКОМ текущего тарифа менеджера (tariff_id/kind/params копируются значением на момент решения — В12: смена тарифа менеджера потом влияет только на новых клиентов, подтверждено тестом). Переназначение занятого клиента = delete+create в транзакции. reject: статус+comment. Письмо AttachmentDecidedMail менеджеру. Тест 6/6, весь sales 88/88, stan 0. Один эскейп на сессию.

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

252 lines
10 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\Mail\Sales\AttachmentDecidedMail;
use App\Models\SalesAttachmentRequest;
use App\Models\SalesClientAssignment;
use App\Models\SalesTariff;
use App\Models\SalesUser;
use App\Models\Tenant;
use App\Services\Sales\SalesAttachmentService;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Mail;
/**
* TDD: SalesAttachmentService::decide() — решение начальника по заявке.
*
* Покрывает Task 2.2:
* - approve → создаёт assignment со snapshot тарифа менеджера
* - snapshot immutability (В12)
* - reject → status='rejected', comment, письмо, без assignment
* - reassign → старый assignment удалён, создан новый
* - manager decide → AuthorizationException
* - менеджер без тарифа → assignment без тарифа
*
* DB_DATABASE=liderra_testing; DatabaseTransactions — откат в конце теста.
*/
uses(DatabaseTransactions::class);
// ── helpers ──────────────────────────────────────────────────────────────────
function makeDecideManager(array $attrs = []): SalesUser
{
return SalesUser::create(array_merge([
'name' => 'Менеджер '.uniqid(),
'email' => 'mgr2'.uniqid().'@sales.local',
'password' => bcrypt('secret'),
'role' => 'manager',
'is_active' => true,
], $attrs));
}
function makeDecideHead(array $attrs = []): SalesUser
{
return SalesUser::create(array_merge([
'name' => 'Руководитель '.uniqid(),
'email' => 'head2'.uniqid().'@sales.local',
'password' => bcrypt('secret'),
'role' => 'head',
'is_active' => true,
], $attrs));
}
function makeTariff(array $attrs = []): SalesTariff
{
return SalesTariff::create(array_merge([
'name' => 'Тариф '.uniqid(),
'kind' => 'topup_step',
'params' => ['step_rub' => 1000, 'reward_pct' => 5],
'is_active' => true,
], $attrs));
}
function makePendingRequest(SalesUser $manager, Tenant $tenant): SalesAttachmentRequest
{
return SalesAttachmentRequest::create([
'sales_user_id' => $manager->id,
'login_input' => $tenant->contact_email ?? 'client@test.local',
'tenant_id' => $tenant->id,
'status' => 'pending',
]);
}
function makeDecideTenant(): Tenant
{
return Tenant::factory()->create([
'contact_email' => 'client2'.uniqid().'@example.com',
]);
}
function getService(): SalesAttachmentService
{
return app(SalesAttachmentService::class);
}
// ── approve: свободный клиент ─────────────────────────────────────────────────
test('head approves pending request for free client — assignment created with tariff snapshot', function (): void {
Mail::fake();
$tariff = makeTariff();
$manager = makeDecideManager(['current_tariff_id' => $tariff->id]);
$head = makeDecideHead();
$tenant = makeDecideTenant();
$request = makePendingRequest($manager, $tenant);
$result = getService()->decide($head, $request->id, 'approve');
// Статус заявки изменён
expect($result->status)->toBe('approved')
->and($result->decided_by)->toBe($head->id)
->and($result->decided_at)->not->toBeNull();
// Assignment создан
$assignment = SalesClientAssignment::where('tenant_id', $tenant->id)->first();
expect($assignment)->not->toBeNull()
->and($assignment->sales_user_id)->toBe($manager->id)
->and($assignment->tariff_id)->toBe($tariff->id)
->and($assignment->tariff_kind)->toBe($tariff->kind)
->and($assignment->tariff_params)->toBe($tariff->params);
// Письмо поставлено в очередь
Mail::assertQueued(AttachmentDecidedMail::class, fn ($mail): bool => $mail->hasTo($manager->email));
});
// ── snapshot immutability (В12) ────────────────────────────────────────────────
test('snapshot immutability: changing manager tariff after approve does not affect existing assignment', function (): void {
Mail::fake();
$tariffA = makeTariff(['kind' => 'topup_step', 'params' => ['step_rub' => 500]]);
$tariffB = makeTariff(['kind' => 'percent_oborot', 'params' => ['pct' => 10]]);
$manager = makeDecideManager(['current_tariff_id' => $tariffA->id]);
$head = makeDecideHead();
$tenant = makeDecideTenant();
$request = makePendingRequest($manager, $tenant);
getService()->decide($head, $request->id, 'approve');
// Меняем тариф менеджера после одобрения
$manager->update(['current_tariff_id' => $tariffB->id]);
// Snapshot assignment-а НЕ должен был измениться
$assignment = SalesClientAssignment::where('tenant_id', $tenant->id)->first();
expect($assignment)->not->toBeNull()
->and($assignment->tariff_id)->toBe($tariffA->id)
->and($assignment->tariff_kind)->toBe('topup_step')
->and($assignment->tariff_params)->toBe(['step_rub' => 500]);
});
// ── reject ─────────────────────────────────────────────────────────────────────
test('head rejects pending request — status rejected, comment stored, no assignment, mail queued', function (): void {
Mail::fake();
$manager = makeDecideManager();
$head = makeDecideHead();
$tenant = makeDecideTenant();
$request = makePendingRequest($manager, $tenant);
$result = getService()->decide($head, $request->id, 'reject', 'Клиент уже обслуживается нами напрямую');
expect($result->status)->toBe('rejected')
->and($result->decided_by)->toBe($head->id)
->and($result->decided_at)->not->toBeNull()
->and($result->comment)->toBe('Клиент уже обслуживается нами напрямую');
// Assignment НЕ создан
$assignment = SalesClientAssignment::where('tenant_id', $tenant->id)->first();
expect($assignment)->toBeNull();
// Письмо об отказе поставлено в очередь
Mail::assertQueued(AttachmentDecidedMail::class, fn ($mail): bool => $mail->hasTo($manager->email));
});
// ── reassign ──────────────────────────────────────────────────────────────────
test('reassign: head approves manager B for client already assigned to manager A — old assignment gone, new one created', function (): void {
Mail::fake();
$tariffA = makeTariff(['name' => 'Тариф А '.uniqid()]);
$tariffB = makeTariff(['name' => 'Тариф Б '.uniqid(), 'kind' => 'fix_per_client', 'params' => ['fix_rub' => 2000]]);
$managerA = makeDecideManager(['current_tariff_id' => $tariffA->id]);
$managerB = makeDecideManager(['current_tariff_id' => $tariffB->id]);
$head = makeDecideHead();
$tenant = makeDecideTenant();
// Клиент уже привязан к менеджеру A
SalesClientAssignment::create([
'sales_user_id' => $managerA->id,
'tenant_id' => $tenant->id,
'tariff_id' => $tariffA->id,
'tariff_kind' => $tariffA->kind,
'tariff_params' => $tariffA->params,
'assigned_at' => now(),
]);
// Менеджер B подал заявку на того же клиента
$requestB = makePendingRequest($managerB, $tenant);
$result = getService()->decide($head, $requestB->id, 'approve');
expect($result->status)->toBe('approved');
// Старый assignment менеджера A должен быть удалён
$oldAssignment = SalesClientAssignment::where('tenant_id', $tenant->id)
->where('sales_user_id', $managerA->id)
->first();
expect($oldAssignment)->toBeNull();
// Новый assignment создан для менеджера B
$newAssignment = SalesClientAssignment::where('tenant_id', $tenant->id)->first();
expect($newAssignment)->not->toBeNull()
->and($newAssignment->sales_user_id)->toBe($managerB->id)
->and($newAssignment->tariff_id)->toBe($tariffB->id)
->and($newAssignment->tariff_kind)->toBe('fix_per_client')
->and($newAssignment->tariff_params)->toBe(['fix_rub' => 2000]);
// tenant_id UNIQUE — только одна запись
$count = SalesClientAssignment::where('tenant_id', $tenant->id)->count();
expect($count)->toBe(1);
Mail::assertQueued(AttachmentDecidedMail::class);
});
// ── менеджер не может решать ──────────────────────────────────────────────────
test('manager calling decide throws AuthorizationException', function (): void {
$manager = makeDecideManager();
$head = makeDecideHead();
$tenant = makeDecideTenant();
$request = makePendingRequest($manager, $tenant);
expect(fn () => getService()->decide($manager, $request->id, 'approve'))
->toThrow(AuthorizationException::class);
});
// ── менеджер без тарифа ───────────────────────────────────────────────────────
test('manager with current_tariff_id=null approved — assignment created with null tariff (income 0)', function (): void {
Mail::fake();
$manager = makeDecideManager(['current_tariff_id' => null]);
$head = makeDecideHead();
$tenant = makeDecideTenant();
$request = makePendingRequest($manager, $tenant);
$result = getService()->decide($head, $request->id, 'approve');
expect($result->status)->toBe('approved');
$assignment = SalesClientAssignment::where('tenant_id', $tenant->id)->first();
expect($assignment)->not->toBeNull()
->and($assignment->tariff_id)->toBeNull()
->and($assignment->tariff_kind)->toBeNull()
->and($assignment->tariff_params)->toBe([]);
Mail::assertQueued(AttachmentDecidedMail::class);
});