Files
portal/app/tests/Feature/ClientSms/AudienceBuilderTest.php
T
Дмитрий 3ec722416c feat(смс-клиент): сервисы — цена по ступеням, аудитория, отбор получателей
ClientSmsPricing (ступень по объёму получатели×сегменты, наценка не
применяется), ClientSmsAudienceBuilder (сделки/база/список, tenant-scope
SET LOCAL + явный where, нормализация телефона), ClientSmsRecipientSelector
(стоп-лист→дубль→маршрут), ClientSmsPlan DTO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 20:59:35 +03:00

197 lines
6.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Models\ClientSmsCampaign;
use App\Models\ClientSmsCampaignPhone;
use App\Models\ClientSmsContact;
use App\Models\Deal;
use App\Models\Tenant;
use App\Services\ClientSms\ClientSmsAudienceBuilder;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
function makeClientSmsCampaign(int $tenantId, string $source, ?int $audienceDays = null): ClientSmsCampaign
{
return ClientSmsCampaign::create([
'tenant_id' => $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' => 'МегаФон'],
]);
});