tenant = Tenant::factory()->create(); $this->user = User::factory()->create(['tenant_id' => $this->tenant->id]); $this->actingAs($this->user); }); afterEach(function () { Carbon::setTestNow(); }); /** Авто-СМС: сообщение без кампании. */ function autoLogMessage(int $tenantId, string $phone, string $status, ?string $createdAt = null): ClientSmsMessage { $message = ClientSmsMessage::create([ 'tenant_id' => $tenantId, 'campaign_id' => null, 'phone' => $phone, 'status' => $status, 'cost_rub' => $status === ClientSmsMessage::STATUS_SENT ? '8.50' : '0.00', 'segments' => 1, ]); if ($createdAt !== null) { // Через модель дата перезапишется текущим временем — правим запросом. ClientSmsMessage::whereKey($message->id)->update(['created_at' => $createdAt]); } return $message; } it('журнал показывает только авто-СМС и только за последние 30 дней', function () { $tenantId = (int) $this->tenant->id; autoLogMessage($tenantId, '79990000001', ClientSmsMessage::STATUS_SENT); autoLogMessage($tenantId, '79990000002', ClientSmsMessage::SKIP_OPTOUT); autoLogMessage($tenantId, '79990000003', ClientSmsMessage::SKIP_NO_FUNDS); // Сообщение РАССЫЛКИ в журнал авто-СМС попадать не должно: у него есть кампания. $campaign = ClientSmsCampaign::create([ 'tenant_id' => $tenantId, 'title' => 'Рассылка', 'body' => 'Здравствуйте!', 'sender_name' => 'liderra.ru', 'source' => ClientSmsCampaign::SOURCE_BASE, 'status' => ClientSmsCampaign::STATUS_QUEUED, 'segments' => 1, 'planned_count' => 1, 'total_sms' => 1, 'price_rub_per_sms' => '8.50', 'estimated_cost_rub' => '8.50', 'created_by' => null, ]); ClientSmsMessage::create([ 'tenant_id' => $tenantId, 'campaign_id' => $campaign->id, 'phone' => '79990000004', 'status' => ClientSmsMessage::STATUS_SENT, 'cost_rub' => '8.50', 'segments' => 1, ]); // Авто-СМС старше 30 дней — тоже не должно. autoLogMessage($tenantId, '79990000005', ClientSmsMessage::STATUS_SENT, '2026-06-01 10:00:00'); $response = $this->getJson('/api/sms/auto-log'); $response->assertOk(); expect($response->json('totals.sent'))->toBe(1) ->and($response->json('totals.skipped'))->toBe(2) ->and(collect($response->json('reasons'))->pluck('count', 'reason')->all()) ->toBe([ClientSmsMessage::SKIP_NO_FUNDS => 1, ClientSmsMessage::SKIP_OPTOUT => 1]) ->and(collect($response->json('messages'))->pluck('phone')->all()) ->toBe(['79990000003', '79990000002', '79990000001']); }); it('пробное сообщение считается НЕ ушедшим (В-172)', function () { $tenantId = (int) $this->tenant->id; autoLogMessage($tenantId, '79990000011', ClientSmsMessage::STATUS_SENT); autoLogMessage($tenantId, '79990000012', ClientSmsMessage::STATUS_FAKE_SENT); autoLogMessage($tenantId, '79990000013', ClientSmsMessage::STATUS_FAILED); $response = $this->getJson('/api/sms/auto-log'); $response->assertOk(); // Пробное не ушло абоненту, и денежный счётчик модуля его тоже не считает. expect($response->json('totals.sent'))->toBe(1) ->and($response->json('totals.skipped'))->toBe(2) ->and(collect($response->json('reasons'))->pluck('count', 'reason')->all()) ->toBe([ ClientSmsMessage::STATUS_FAILED => 1, ClientSmsMessage::STATUS_FAKE_SENT => 1, ]); }); it('журнал чужого клиента не показывает', function () { $tenantId = (int) $this->tenant->id; $stranger = Tenant::factory()->create(); autoLogMessage((int) $stranger->id, '79990000021', ClientSmsMessage::STATUS_SENT); autoLogMessage((int) $stranger->id, '79990000022', ClientSmsMessage::SKIP_NO_FUNDS); autoLogMessage($tenantId, '79990000023', ClientSmsMessage::STATUS_SENT); $response = $this->getJson('/api/sms/auto-log'); $response->assertOk(); expect($response->json('totals.sent'))->toBe(1) ->and($response->json('totals.skipped'))->toBe(0) ->and(collect($response->json('messages'))->pluck('phone')->all()) ->toBe(['79990000023']); }); it('числа считаются по всем 30 дням, а список событий урезан сотней (В-170)', function () { $tenantId = (int) $this->tenant->id; for ($i = 1; $i <= 105; $i++) { autoLogMessage($tenantId, '7999000'.str_pad((string) $i, 4, '0', STR_PAD_LEFT), ClientSmsMessage::STATUS_SENT); } autoLogMessage($tenantId, '79990009999', ClientSmsMessage::SKIP_NO_FUNDS); $response = $this->getJson('/api/sms/auto-log'); $response->assertOk(); // Числа — по ВСЕМ событиям окна, а не по показанной сотне. expect($response->json('totals.sent'))->toBe(105) ->and($response->json('totals.skipped'))->toBe(1) ->and($response->json('messages'))->toHaveCount(100) // Человеку надо сказать, что список урезан, иначе он решит, что видит всё (В-48). ->and($response->json('messages_limit'))->toBe(100) ->and($response->json('messages_truncated'))->toBeTrue(); });