— PHPStan не понимает привязку $this * в Pest (тот же приём, что sms_phone() в SmsRecipientSelectorTest). * * @param array $phones */ function sms_campaign_with_phones(array $phones): SalesSmsCampaign { $sender = SalesSmsSender::create([ 'name' => 'liderra.ru', 'provider_key' => 'fake', 'status' => 'active', ]); foreach ($phones as $p) { SalesAdAudiencePhone::create([ 'phone' => $p, 'expires_at' => now()->addDays(30), 'state' => 'active', 'operator' => 'МТС', 'phone_type' => 'mobile', ]); } return SalesSmsCampaign::create([ 'title' => 'Прогрев', 'body' => 'Здравствуйте!', 'created_by' => 1, 'sender_id' => $sender->id, 'status' => SalesSmsCampaign::STATUS_QUEUED, ]); } it('отправляет каждому годному номеру и закрывает кампанию', function () { $campaign = sms_campaign_with_phones(['79990000001', '79990000002']); (new SendSmsCampaignJob($campaign->id, ['79990000001', '79990000002']))->handle(); $campaign->refresh(); expect($campaign->status)->toBe(SalesSmsCampaign::STATUS_DONE) ->and($campaign->sent_count)->toBe(2) ->and($campaign->messages)->toHaveCount(2); }); it('пропущенные номера попадают в журнал с причиной, но не в отправку', function () { $campaign = sms_campaign_with_phones(['79990000003', '79990000004']); SalesSmsOptout::create(['phone' => '79990000004', 'reason' => 'manual', 'created_at' => now()]); (new SendSmsCampaignJob($campaign->id, ['79990000003', '79990000004']))->handle(); $campaign->refresh(); expect($campaign->sent_count)->toBe(1) ->and($campaign->skipped_count)->toBe(1); $skipped = SalesSmsMessage::where('campaign_id', $campaign->id) ->where('phone', '79990000004')->first(); expect($skipped->status)->toBe(SalesSmsMessage::SKIP_OPTOUT) ->and($skipped->cost_kopecks)->toBe(0); }); it('не стартует без согласованного имени отправителя', function () { $campaign = sms_campaign_with_phones(['79990000005']); $campaign->sender->update(['status' => 'pending']); (new SendSmsCampaignJob($campaign->id, ['79990000005']))->handle(); $campaign->refresh(); expect($campaign->status)->toBe(SalesSmsCampaign::STATUS_FAILED) ->and($campaign->last_error)->toContain('имя отправителя') ->and($campaign->messages)->toHaveCount(0); }); it('повторный запуск не создаёт вторых сообщений', function () { $campaign = sms_campaign_with_phones(['79990000006']); (new SendSmsCampaignJob($campaign->id, ['79990000006']))->handle(); (new SendSmsCampaignJob($campaign->id, ['79990000006']))->handle(); expect(SalesSmsMessage::where('campaign_id', $campaign->id)->count())->toBe(1); }); // ── B3: отправка СМС пишет эпизод прогрева (летопись витрины) ──────────────── // // СМС — разовая акция: один эпизод на ФИРМУ в кампании (у фирмы может быть // несколько номеров в одной рассылке — это одна «стрельба», не N). Повтор в // новой кампании = новый эпизод («СМС ×N» = число волн). function sms_sender_campaign(): SalesSmsCampaign { // name — VARCHAR(11) (буквенный sender ID), уникум не влезает; две кампании // одного теста («СМС ×2») переиспользуют один отправитель через firstOrCreate // (unique tenant_id+name+provider_key). $sender = SalesSmsSender::firstOrCreate( ['name' => 'liderra.ru', 'provider_key' => 'fake'], ['status' => 'active'], ); return SalesSmsCampaign::create([ 'title' => 'Прогрев', 'body' => 'Здравствуйте!', 'created_by' => 1, 'sender_id' => $sender->id, 'status' => SalesSmsCampaign::STATUS_QUEUED, ]); } function sms_firm_phone(SalesAdAudienceFirm $firm, string $phone): void { SalesAdAudiencePhone::create([ 'phone' => $phone, 'firm_id' => $firm->id, 'expires_at' => now()->addDays(30), 'state' => 'active', 'operator' => 'МТС', 'phone_type' => 'mobile', ]); } it('успешная отправка СМС пишет мгновенный эпизод прогрева фирме номера', function () { $firm = SalesAdAudienceFirm::create(['firm_name' => 'Тест', 'warmup_started_at' => now()]); sms_firm_phone($firm, '79990000101'); (new SendSmsCampaignJob(sms_sender_campaign()->id, ['79990000101']))->handle(); $ep = SalesAdAudienceWarmingEpisode::where('firm_id', $firm->id)->where('channel', 'sms')->sole(); expect($ep->started_at->timestamp)->toBe($ep->ended_at->timestamp); }); it('фирма с двумя номерами в ОДНОЙ кампании даёт один эпизод СМС', function () { $firm = SalesAdAudienceFirm::create(['firm_name' => 'Тест', 'warmup_started_at' => now()]); sms_firm_phone($firm, '79990000111'); sms_firm_phone($firm, '79990000112'); (new SendSmsCampaignJob(sms_sender_campaign()->id, ['79990000111', '79990000112']))->handle(); expect(SalesAdAudienceWarmingEpisode::where('firm_id', $firm->id)->where('channel', 'sms')->count())->toBe(1); }); it('две кампании той же фирме дают два эпизода (СМС ×2)', function () { $firm = SalesAdAudienceFirm::create(['firm_name' => 'Тест', 'warmup_started_at' => now()]); sms_firm_phone($firm, '79990000121'); (new SendSmsCampaignJob(sms_sender_campaign()->id, ['79990000121']))->handle(); (new SendSmsCampaignJob(sms_sender_campaign()->id, ['79990000121']))->handle(); expect(SalesAdAudienceWarmingEpisode::where('firm_id', $firm->id)->where('channel', 'sms')->count())->toBe(2); }); it('номер без фирмы (сирота) эпизод не пишет', function () { // Номер без firm_id (сирота до v8.77) отправляется, но эпизод фирме писать некому. SalesAdAudiencePhone::create([ 'phone' => '79990000131', 'expires_at' => now()->addDays(30), 'state' => 'active', 'operator' => 'МТС', 'phone_type' => 'mobile', ]); (new SendSmsCampaignJob(sms_sender_campaign()->id, ['79990000131']))->handle(); expect(SalesAdAudienceWarmingEpisode::where('channel', 'sms')->count())->toBe(0); }); it('при нехватке денег у клиента не уходит ни одной СМС', function () { $tenant = Tenant::factory()->create(['balance_rub' => '1.00']); $campaign = sms_campaign_with_phones(['79990000007']); $campaign->update(['tenant_id' => $tenant->id, 'estimated_cost_kopecks' => 50000]); (new SendSmsCampaignJob($campaign->id, ['79990000007']))->handle(); $campaign->refresh(); expect($campaign->status)->toBe(SalesSmsCampaign::STATUS_FAILED) ->and($campaign->sent_count)->toBe(0) ->and($campaign->last_error)->toContain('Не хватило денег'); $row = SalesSmsMessage::where('campaign_id', $campaign->id)->first(); expect($row->status)->toBe(SalesSmsMessage::SKIP_NO_FUNDS); // Деньги не тронуты: списания не было, потому что его не хватало. expect((string) $tenant->refresh()->balance_rub)->toBe('1.00'); });