'MV '.uniqid(), 'email' => 'mv'.uniqid().'@s.local', 'password' => bcrypt('secret'), 'role' => $role, 'is_active' => true, ]); } /** @return list */ function mv_moves(SalesProspect $p): array { return SalesProspectMove::query() ->where('prospect_id', $p->id) ->orderBy('id') ->get() ->map(fn (SalesProspectMove $m) => [ 'from' => $m->from_stage, 'to' => $m->to_stage, 'by' => $m->sales_user_id, ]) ->all(); } // ── событие модели: журнал движений ─────────────────────────────────────────── test('заведение карточки пишет первое движение — из ниоткуда в стадию', function () { $mgr = mv_user(); $p = SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'new']); expect(mv_moves($p))->toBe([['from' => null, 'to' => 'new', 'by' => null]]); }); test('смена стадии пишет движение и заполняет prev_stage', function () { $mgr = mv_user(); $p = SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'new']); $p->stage = 'in_work'; $p->save(); expect($p->prev_stage)->toBe('new'); expect(mv_moves($p))->toBe([ ['from' => null, 'to' => 'new', 'by' => null], ['from' => 'new', 'to' => 'in_work', 'by' => null], ]); }); test('сохранение БЕЗ смены стадии движения не пишет', function () { $mgr = mv_user(); $p = SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'new']); $p->city = 'Томск'; $p->save(); expect(mv_moves($p))->toHaveCount(1); }); test('несколько переездов подряд ложатся цепочкой, prev_stage — только последний', function () { $mgr = mv_user(); $p = SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'new']); foreach (['in_work', 'negotiation', 'manual_testing'] as $stage) { $p->stage = $stage; $p->save(); } expect($p->prev_stage)->toBe('negotiation'); expect(array_column(mv_moves($p), 'to'))->toBe(['new', 'in_work', 'negotiation', 'manual_testing']); }); // ── движения пишутся ОТОВСЮДУ: и от менеджера, и от джобы ───────────────────── test('результат разговора через API пишет движение с автором-менеджером', function () { $mgr = mv_user(); $p = SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'in_work']); $this->actingAs($mgr, 'sales') ->patchJson("/api/sales/prospects/{$p->id}", [ 'action' => 'negotiation', 'next_call_at' => '2026-08-05T12:00:00', ]) ->assertOk(); $moves = mv_moves($p); expect(end($moves))->toBe(['from' => 'in_work', 'to' => 'negotiation', 'by' => $mgr->id]); }); test('автопереезд по деньгам пишет движение без автора', function () { $mgr = mv_user(); $tenant = Tenant::factory()->create(); $p = SalesProspect::factory()->for($mgr, 'salesUser')->create([ 'stage' => 'registered', 'linked_tenant_id' => $tenant->id, ]); DB::table('balance_transactions')->insert([ 'tenant_id' => $tenant->id, 'type' => 'topup', 'amount_rub' => 5000, 'description' => 'пополнение', 'created_at' => now(), ]); (new SalesProspectsAdvanceJob)->handle(); $moves = mv_moves($p); expect(end($moves))->toBe(['from' => 'registered', 'to' => 'topped_up', 'by' => null]); });