'Менеджер '.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 — валидный новый вид (используется только как НОВЫЙ текущий тариф менеджера; // снимок привязки должен остаться от tariffA). $tariffB = makeTariff(['kind' => 'daily_salary', 'params' => []]); $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' => 'topup_step', 'params' => ['threshold' => 0, 'reward' => 2000, 'periods' => []]]); $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('topup_step') // Снимок = params tariffB (сравниваем по значениям — JSONB не хранит порядок ключей). ->and((float) $newAssignment->tariff_params['reward'])->toBe(2000.0) ->and((float) $newAssignment->tariff_params['threshold'])->toBe(0.0) ->and($newAssignment->tariff_params['periods'])->toBe([]); // 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); });