$tenantId, 'title' => 'Т', 'body' => 'Здравствуйте!', 'sender_name' => 'LIDERRA', 'source' => $source, 'audience_days' => $audienceDays, 'status' => ClientSmsCampaign::STATUS_DRAFT, 'segments' => 1, 'planned_count' => 0, 'sent_count' => 0, 'total_sms' => 0, 'price_rub_per_sms' => '8.50', 'estimated_cost_rub' => '0.00', 'actual_cost_rub' => null, 'created_by' => null, ]); } it('builds audience from deals honoring window, soft-delete and duplicate master', function () { $tenant = Tenant::factory()->create(); // в окне, мастер-лид — должен попасть Deal::factory()->create([ 'tenant_id' => $tenant->id, 'phone' => '79990000001', 'phone_operator' => 'МТС', 'received_at' => now()->subDays(2), ]); // вне окна — не должен попасть Deal::factory()->create([ 'tenant_id' => $tenant->id, 'phone' => '79990000002', 'phone_operator' => 'МТС', 'received_at' => now()->subDays(30), ]); // мягко удалён — не должен попасть $deleted = Deal::factory()->create([ 'tenant_id' => $tenant->id, 'phone' => '79990000003', 'phone_operator' => 'МТС', 'received_at' => now()->subDays(1), ]); $deleted->delete(); // дубликат другого лида (duplicate_of_id не null) — не должен попасть Deal::factory()->create([ 'tenant_id' => $tenant->id, 'phone' => '79990000004', 'phone_operator' => 'МТС', 'duplicate_of_id' => 999999, 'received_at' => now()->subDays(1), ]); $campaign = makeClientSmsCampaign($tenant->id, ClientSmsCampaign::SOURCE_DEALS, 5); $result = app(ClientSmsAudienceBuilder::class)->build($campaign); expect($result)->toBe([ ['phone' => '79990000001', 'operator' => 'МТС'], ]); }); it('builds audience from own contact base (source=base)', function () { $tenant = Tenant::factory()->create(); ClientSmsContact::create([ 'tenant_id' => $tenant->id, 'phone' => '79990000011', 'name' => 'Иван', 'operator' => 'Билайн', ]); ClientSmsContact::create([ 'tenant_id' => $tenant->id, 'phone' => '79990000012', 'name' => 'Пётр', 'operator' => null, ]); $campaign = makeClientSmsCampaign($tenant->id, ClientSmsCampaign::SOURCE_BASE); $result = app(ClientSmsAudienceBuilder::class)->build($campaign); expect($result)->toEqualCanonicalizing([ ['phone' => '79990000011', 'operator' => 'Билайн'], ['phone' => '79990000012', 'operator' => null], ]); }); it('builds audience from manual campaign phone list honoring expiry (source=manual)', function () { $tenant = Tenant::factory()->create(); $campaign = makeClientSmsCampaign($tenant->id, ClientSmsCampaign::SOURCE_MANUAL); ClientSmsCampaignPhone::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'phone' => '79990000021', 'expires_at' => null, ]); ClientSmsCampaignPhone::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'phone' => '79990000022', 'expires_at' => now()->addDays(3), ]); // истёкший — не должен попасть ClientSmsCampaignPhone::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'phone' => '79990000023', 'expires_at' => now()->subDay(), ]); $result = app(ClientSmsAudienceBuilder::class)->build($campaign); expect($result)->toEqualCanonicalizing([ ['phone' => '79990000021', 'operator' => null], ['phone' => '79990000022', 'operator' => null], ]); }); it('never leaks phones across tenants', function () { $tenantA = Tenant::factory()->create(); $tenantB = Tenant::factory()->create(); Deal::factory()->create([ 'tenant_id' => $tenantA->id, 'phone' => '79990000031', 'phone_operator' => 'МТС', 'received_at' => now()->subDays(1), ]); Deal::factory()->create([ 'tenant_id' => $tenantB->id, 'phone' => '79990000032', 'phone_operator' => 'МТС', 'received_at' => now()->subDays(1), ]); ClientSmsContact::create([ 'tenant_id' => $tenantB->id, 'phone' => '79990000033', 'name' => null, 'operator' => null, ]); $campaignDeals = makeClientSmsCampaign($tenantA->id, ClientSmsCampaign::SOURCE_DEALS, 5); $campaignBase = makeClientSmsCampaign($tenantA->id, ClientSmsCampaign::SOURCE_BASE); $resultDeals = app(ClientSmsAudienceBuilder::class)->build($campaignDeals); $resultBase = app(ClientSmsAudienceBuilder::class)->build($campaignBase); expect($resultDeals)->toBe([ ['phone' => '79990000031', 'operator' => 'МТС'], ]) ->and($resultBase)->toBe([]); }); it('dedups phones across formats preferring a non-null operator, normalized to bare 11 digits', function () { $tenant = Tenant::factory()->create(); // тот же телефон в разных форматах ввода, разные сделки Deal::factory()->create([ 'tenant_id' => $tenant->id, 'phone' => '89991112233', 'phone_operator' => null, 'received_at' => now()->subDays(1), ]); Deal::factory()->create([ 'tenant_id' => $tenant->id, 'phone' => '+79991112233', 'phone_operator' => 'МегаФон', 'received_at' => now()->subDays(2), ]); $campaign = makeClientSmsCampaign($tenant->id, ClientSmsCampaign::SOURCE_DEALS, 5); $result = app(ClientSmsAudienceBuilder::class)->build($campaign); expect($result)->toBe([ ['phone' => '79991112233', 'operator' => 'МегаФон'], ]); });