$tenantId, 'title' => 'Проверка чистки снимка', 'body' => 'Текст', 'sender_name' => 'LIDERRA', 'source' => ClientSmsCampaign::SOURCE_MANUAL, 'status' => $status, 'idempotency_key' => 'purge-'.uniqid(), 'segments' => 1, 'planned_count' => 1, 'sent_count' => 1, 'total_sms' => 1, 'price_rub_per_sms' => '8.50', 'estimated_cost_rub' => '8.50', ]); } /** Строка снимка с заданным возрастом. Дату правим запросом — модель ставит своё время. */ function purgePhone(int $tenantId, int $campaignId, string $phone, string $createdAt): ClientSmsCampaignPhone { $row = ClientSmsCampaignPhone::create([ 'tenant_id' => $tenantId, 'campaign_id' => $campaignId, 'phone' => $phone, ]); ClientSmsCampaignPhone::whereKey($row->id)->update(['created_at' => $createdAt]); return $row; } it('4.12 — снимок старше 90 дней уходит, а свежий, рассылка и её итоги остаются', function () { $tenant = Tenant::factory()->create(); $campaign = purgeCampaign($tenant->id); // Граница — 90 дней от 15.08, то есть 17.05. Слева от неё чистим, справа нет. $old = purgePhone($tenant->id, $campaign->id, '79990000001', '2026-05-16 03:00:00'); $fresh = purgePhone($tenant->id, $campaign->id, '79990000002', '2026-05-18 03:00:00'); // Итог рассылки: журнал сообщений — то, что владелец велел ОСТАВИТЬ. $message = ClientSmsMessage::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'phone' => '79990000001', 'status' => ClientSmsMessage::STATUS_SENT, 'cost_rub' => '8.50', 'segments' => 1, ]); ClientSmsMessage::whereKey($message->id)->update(['created_at' => '2026-05-16 03:05:00']); $this->artisan('client-sms:purge-snapshots')->assertSuccessful(); expect(ClientSmsCampaignPhone::whereKey($old->id)->exists())->toBeFalse() ->and(ClientSmsCampaignPhone::whereKey($fresh->id)->exists())->toBeTrue() // Вторая половина обязательства: рассылка и её итоги на месте. ->and(ClientSmsCampaign::whereKey($campaign->id)->exists())->toBeTrue() ->and(ClientSmsMessage::whereKey($message->id)->exists())->toBeTrue(); }); it('4.12 — чистит у ВСЕХ клиентов, а не только у одного', function () { $first = Tenant::factory()->create(); $second = Tenant::factory()->create(); $firstRow = purgePhone($first->id, purgeCampaign($first->id)->id, '79990000011', '2026-01-01 03:00:00'); $secondRow = purgePhone($second->id, purgeCampaign($second->id)->id, '79990000012', '2026-01-01 03:00:00'); $this->artisan('client-sms:purge-snapshots')->assertSuccessful(); // Команда ходит без пометки клиента — если она её где-то поставит, второй клиент уцелеет // и обязательство будет выполнено только для первого (класс В-114, но наоборот). expect(ClientSmsCampaignPhone::whereKey($firstRow->id)->exists())->toBeFalse() ->and(ClientSmsCampaignPhone::whereKey($secondRow->id)->exists())->toBeFalse(); }); it('4.12 — уносит больше одной пачки за заход: 5001 строка снимка чистится целиком', function () { $tenant = Tenant::factory()->create(); $campaign = purgeCampaign($tenant->id); // Пачка — 5000 строк. Без цикла команда унесла бы ровно 5000 и отрапортовала успехом: // одна строка персональных данных осталась бы жить вечно (класс В-121, «успех с остатком»). $rows = []; for ($i = 0; $i < 5001; $i++) { $rows[] = [ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'phone' => '7999'.str_pad((string) $i, 7, '0', STR_PAD_LEFT), 'created_at' => '2026-01-01 03:00:00', 'updated_at' => '2026-01-01 03:00:00', ]; } foreach (array_chunk($rows, 1000) as $chunk) { ClientSmsCampaignPhone::insert($chunk); } $this->artisan('client-sms:purge-snapshots')->assertSuccessful(); expect(ClientSmsCampaignPhone::where('campaign_id', $campaign->id)->count())->toBe(0); }); it('4.12 — срок можно задать при ручном запуске, он не зашит в запрос намертво', function () { $tenant = Tenant::factory()->create(); $campaign = purgeCampaign($tenant->id); $row = purgePhone($tenant->id, $campaign->id, '79990000021', '2026-07-01 03:00:00'); // 45 дней от 15.08 — внутри девяноста, при обычном запуске строка осталась бы. $this->artisan('client-sms:purge-snapshots', ['--days' => 10])->assertSuccessful(); expect(ClientSmsCampaignPhone::whereKey($row->id)->exists())->toBeFalse(); }); it('4.12 — нулевой срок отклоняется: он снёс бы снимки рассылок, которые идут сейчас', function () { $tenant = Tenant::factory()->create(); $campaign = purgeCampaign($tenant->id); $today = purgePhone($tenant->id, $campaign->id, '79990000041', '2026-08-15 03:00:00'); $this->artisan('client-sms:purge-snapshots', ['--days' => 0])->assertFailed(); expect(ClientSmsCampaignPhone::whereKey($today->id)->exists())->toBeTrue(); }); it('4.12 — про снимок НЕзакончившейся рассылки говорит вслух, а не молчит (В-176)', function () { $tenant = Tenant::factory()->create(); $live = purgeCampaign($tenant->id, ClientSmsCampaign::STATUS_SENDING); purgePhone($tenant->id, $live->id, '79990000031', '2026-01-01 03:00:00'); // Рассылки старше 90 дней в состоянии «идёт» в норме не бывает: зависшую срывает // сторож через час. Значит это странность, и человек обязан её увидеть. $this->artisan('client-sms:purge-snapshots') ->expectsOutputToContain('не закончены') ->assertSuccessful(); });