53ebcd9fe0
Стадию меняют два разных места — результат разговора менеджера и автопереезд по деньгам. Джоба не оставляла следа вообще, поэтому «что менялось за день» было не из чего построить. Запись движения перенесена в событие модели: один шов на всех, включая любой будущий третий источник. - sales_prospect_moves — журнал всех движений (append-only, с GRANT'ами ролям); - prev_stage на карточке — откуда приехала (кормит счётчик «69/1» у «Отказа»); - stage += 'trash' — место под колонку «Корзина». Сторож проверен вырезанием: без записи движения краснеют 4 теста, в том числе тест джобы и тест API.
122 lines
4.2 KiB
PHP
122 lines
4.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\Sales\SalesProspectsAdvanceJob;
|
|
use App\Models\SalesProspect;
|
|
use App\Models\SalesProspectMove;
|
|
use App\Models\SalesUser;
|
|
use App\Models\Tenant;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
uses(DatabaseTransactions::class);
|
|
|
|
function mv_user(string $role = 'manager'): SalesUser
|
|
{
|
|
return SalesUser::create([
|
|
'name' => 'MV '.uniqid(), 'email' => 'mv'.uniqid().'@s.local',
|
|
'password' => bcrypt('secret'), 'role' => $role, 'is_active' => true,
|
|
]);
|
|
}
|
|
|
|
/** @return list<array{from: ?string, to: string, by: ?int}> */
|
|
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]);
|
|
});
|