diff --git a/app/app/Http/Controllers/Api/Sales/SalesProspectController.php b/app/app/Http/Controllers/Api/Sales/SalesProspectController.php index 525a87ce..d167c48a 100644 --- a/app/app/Http/Controllers/Api/Sales/SalesProspectController.php +++ b/app/app/Http/Controllers/Api/Sales/SalesProspectController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Http\Controllers\Api\Sales; use App\Http\Controllers\Controller; +use App\Models\SalesAdAudienceFirm; use App\Models\SalesProspect; use App\Models\SalesProspectNote; use App\Models\SalesUser; @@ -62,7 +63,9 @@ class SalesProspectController extends Controller $query->where('source', (string) $request->query('source')); } - $prospects = $query->get()->map(fn (SalesProspect $p) => $this->row($p))->all(); + $all = $query->get(); + $warmingByProspect = $this->warmingByProspect($all->pluck('id')->all()); + $prospects = $all->map(fn (SalesProspect $p) => $this->row($p, $warmingByProspect))->all(); // Группировка по стадии (для колонок), все ключи присутствуют. $byStage = array_fill_keys(self::STAGES, []); @@ -520,8 +523,86 @@ class SalesProspectController extends Controller $prospect->reason = null; } - /** @return array */ - private function row(SalesProspect $p): array + /** + * Состояние прогрева по проспектам: одна выборка фирм по prospect_id. + * Фирмы — на pgsql_supplier, проспекты — на дефолтном, сопоставление в PHP + * (JOIN между соединениями невозможен). Используется только в index() — + * канбан воронки; show/store/update отдают безобидный дефолт через row(). + * + * @param array $prospectIds + * @return array}> + */ + private function warmingByProspect(array $prospectIds): array + { + if ($prospectIds === []) { + return []; + } + + $firms = SalesAdAudienceFirm::query() + ->with('phones') + ->whereIn('prospect_id', $prospectIds) + ->get(); + + // sms: номера фирм, попавшие в реально отправленную рассылку (status='sent'; + // 'fake_sent' — песочница, в счёт не идёт). + $phoneToProspect = []; + foreach ($firms as $firm) { + foreach ($firm->phones as $ph) { + $phoneToProspect[$ph->phone] = $firm->prospect_id; + } + } + $smsProspectIds = []; + if ($phoneToProspect !== []) { + $sent = SalesAdAudienceFirm::query()->getConnection() + ->table('sales_sms_messages') + ->where('status', 'sent') + ->whereIn('phone', array_keys($phoneToProspect)) + ->distinct() + ->pluck('phone'); + foreach ($sent as $phone) { + if (isset($phoneToProspect[$phone])) { + $smsProspectIds[$phoneToProspect[$phone]] = true; + } + } + } + + $out = []; + foreach ($firms as $firm) { + if ($firm->prospect_id === null) { + continue; + } + + $onAny = $firm->ch_yandex || $firm->ch_vk || $firm->ch_mts; + $hasActivePhone = $firm->phones->contains(fn ($p): bool => $p->state === 'active'); + + $channels = []; + if ($firm->ch_yandex) { + $channels[] = 'yandex'; + } + if ($firm->ch_vk) { + $channels[] = 'vk'; + } + if ($firm->ch_mts) { + $channels[] = 'mts'; + } + if (isset($smsProspectIds[$firm->prospect_id])) { + $channels[] = 'sms'; + } + + $out[$firm->prospect_id] = [ + 'active' => $onAny && $firm->stopped_at === null && $hasActivePhone, + 'channels' => $channels, + ]; + } + + return $out; + } + + /** + * @param array}> $warmingByProspect + * @return array + */ + private function row(SalesProspect $p, array $warmingByProspect = []): array { return [ 'id' => $p->id, @@ -541,6 +622,7 @@ class SalesProspectController extends Controller 'reason' => $p->reason, 'registered_email' => $p->registered_email, 'notes' => $p->notes, + 'warming' => $warmingByProspect[$p->id] ?? ['active' => false, 'channels' => []], ]; } } diff --git a/app/phpstan-baseline.neon b/app/phpstan-baseline.neon index 2d402127..b888ec39 100644 --- a/app/phpstan-baseline.neon +++ b/app/phpstan-baseline.neon @@ -3954,6 +3954,12 @@ parameters: count: 6 path: tests/Feature/Sales/SalesIntegrationApiTest.php + - + message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:actingAs\(\)\.$#' + identifier: method.notFound + count: 2 + path: tests/Feature/Sales/ProspectWarmingBadgeTest.php + - message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:actingAs\(\)\.$#' identifier: method.notFound diff --git a/app/tests/Feature/Sales/ProspectWarmingBadgeTest.php b/app/tests/Feature/Sales/ProspectWarmingBadgeTest.php new file mode 100644 index 00000000..4cf000ce --- /dev/null +++ b/app/tests/Feature/Sales/ProspectWarmingBadgeTest.php @@ -0,0 +1,112 @@ + 'Менеджер '.uniqid(), + 'email' => 'pwb'.uniqid().'@sales.local', + 'password' => bcrypt('secret'), + 'role' => 'manager', + 'is_active' => true, + ]); +} + +it('карточка проспекта несёт состояние прогрева и каналы', function () { + $manager = pwb_manager(); + $prospect = SalesProspect::create([ + 'sales_user_id' => $manager->id, + 'stage' => 'new', + 'source' => 'search', + 'firm_name' => 'Гарант', + ]); + $firm = SalesAdAudienceFirm::create([ + 'firm_inn' => '6678048177', + 'firm_name' => 'Гарант', + 'warmup_started_at' => now()->subDays(2), + 'ch_yandex' => true, + 'ch_vk' => true, + 'prospect_id' => $prospect->id, + 'assigned_at' => now(), + ]); + SalesAdAudiencePhone::create([ + 'phone' => '79000000002', + 'firm_id' => $firm->id, + 'firm_inn' => '6678048177', + 'state' => 'active', + 'expires_at' => now()->addDays(5), + 'added_at' => now(), + ]); + + $row = $this->actingAs($manager, 'sales') + ->getJson('/api/sales/prospects') + ->assertOk() + ->json('prospects.0'); + + expect($row['warming']['active'])->toBeTrue(); + expect($row['warming']['channels'])->toContain('yandex'); + expect($row['warming']['channels'])->toContain('vk'); +}); + +it('карточка проспекта показывает канал sms, если номер попал в отправленную рассылку', function () { + $manager = pwb_manager(); + $prospect = SalesProspect::create([ + 'sales_user_id' => $manager->id, + 'stage' => 'new', + 'source' => 'search', + 'firm_name' => 'Новодент', + ]); + $firm = SalesAdAudienceFirm::create([ + 'firm_inn' => '6673240656', + 'firm_name' => 'Новодент', + 'warmup_started_at' => now()->subDay(), + 'ch_yandex' => true, + 'prospect_id' => $prospect->id, + 'assigned_at' => now(), + ]); + SalesAdAudiencePhone::create([ + 'phone' => '79220216061', + 'firm_id' => $firm->id, + 'firm_inn' => '6673240656', + 'state' => 'active', + 'expires_at' => now()->addDays(10), + 'added_at' => now(), + ]); + + $campaignId = DB::connection('pgsql_supplier')->table('sales_sms_campaigns')->insertGetId([ + 'title' => 'т', 'body' => 'т', 'status' => 'done', 'created_by' => $manager->id, + 'created_at' => now(), 'updated_at' => now(), + ]); + DB::connection('pgsql_supplier')->table('sales_sms_messages')->insert([ + 'campaign_id' => $campaignId, 'phone' => '79220216061', 'status' => 'sent', + ]); + + $row = $this->actingAs($manager, 'sales') + ->getJson('/api/sales/prospects') + ->assertOk() + ->json('prospects.0'); + + expect($row['warming']['channels'])->toContain('sms'); +});