tenant = Tenant::factory()->create(); }); /** Строка журнала: сколько СМС, какой статус, когда (UTC). */ function volumeMessage(int $tenantId, string $status, ?int $segments, string $atUtc, array $extra = []): ClientSmsMessage { $at = CarbonImmutable::parse($atUtc, 'UTC'); $message = ClientSmsMessage::create($extra + [ 'tenant_id' => $tenantId, 'phone' => '79990000001', 'status' => $status, 'segments' => $segments, 'cost_rub' => '0.00', ]); // created_at ставим отдельно: модель проставляет своё время при создании. ClientSmsMessage::where('id', $message->id)->update(['created_at' => $at]); return $message->refresh(); } it('3.9 — считает СМС, а не сообщения: длинное письмо это два', function () { Carbon::setTestNow(CarbonImmutable::parse('2026-08-10 09:00:00', 'UTC')); volumeMessage($this->tenant->id, ClientSmsMessage::STATUS_SENT, 1, '2026-08-02 09:00:00'); volumeMessage($this->tenant->id, ClientSmsMessage::STATUS_SENT, 2, '2026-08-03 09:00:00'); expect(app(ClientSmsVolumeCounter::class)->sentThisMonth((int) $this->tenant->id))->toBe(3); Carbon::setTestNow(); }); it('3.9 — счётчик один на обе дороги: рассылка и авто-СМС складываются', function () { Carbon::setTestNow(CarbonImmutable::parse('2026-08-10 09:00:00', 'UTC')); volumeMessage($this->tenant->id, ClientSmsMessage::STATUS_SENT, 1, '2026-08-02 09:00:00'); volumeMessage($this->tenant->id, ClientSmsMessage::STATUS_SENT, 1, '2026-08-03 09:00:00', ['deal_id' => null]); expect(app(ClientSmsVolumeCounter::class)->sentThisMonth((int) $this->tenant->id))->toBe(2); Carbon::setTestNow(); }); it('3.10 — песочные отправки в счёт не идут', function () { Carbon::setTestNow(CarbonImmutable::parse('2026-08-10 09:00:00', 'UTC')); volumeMessage($this->tenant->id, ClientSmsMessage::STATUS_SENT, 1, '2026-08-02 09:00:00'); volumeMessage($this->tenant->id, ClientSmsMessage::STATUS_FAKE_SENT, 5, '2026-08-03 09:00:00'); // Не ушедшее — тем более не в счёт: за него не платили. volumeMessage($this->tenant->id, ClientSmsMessage::STATUS_FAILED, 3, '2026-08-04 09:00:00'); volumeMessage($this->tenant->id, ClientSmsMessage::SKIP_OPTOUT, 3, '2026-08-05 09:00:00'); expect(app(ClientSmsVolumeCounter::class)->sentThisMonth((int) $this->tenant->id))->toBe(1); Carbon::setTestNow(); }); it('3.9 — граница месяца по Москве: полночь 1-го числа уже новый месяц', function () { // 31 июля 21:30 UTC — это 1 августа 00:30 в Москве, то есть УЖЕ август. // 31 июля 20:30 UTC — это 31 июля 23:30 в Москве, то есть ещё июль. Carbon::setTestNow(CarbonImmutable::parse('2026-08-01 06:00:00', 'UTC')); volumeMessage($this->tenant->id, ClientSmsMessage::STATUS_SENT, 1, '2026-07-31 20:30:00'); // июль volumeMessage($this->tenant->id, ClientSmsMessage::STATUS_SENT, 1, '2026-07-31 21:30:00'); // август expect(app(ClientSmsVolumeCounter::class)->sentThisMonth((int) $this->tenant->id))->toBe(1); Carbon::setTestNow(); }); it('3.9 — старая строка без числа СМС считается за одну (В-109)', function () { Carbon::setTestNow(CarbonImmutable::parse('2026-08-10 09:00:00', 'UTC')); volumeMessage($this->tenant->id, ClientSmsMessage::STATUS_SENT, null, '2026-08-02 09:00:00'); expect(app(ClientSmsVolumeCounter::class)->sentThisMonth((int) $this->tenant->id))->toBe(1); Carbon::setTestNow(); }); it('3.9 — чужие отправки в свой счётчик не попадают', function () { Carbon::setTestNow(CarbonImmutable::parse('2026-08-10 09:00:00', 'UTC')); $other = Tenant::factory()->create(); volumeMessage($this->tenant->id, ClientSmsMessage::STATUS_SENT, 1, '2026-08-02 09:00:00'); volumeMessage($other->id, ClientSmsMessage::STATUS_SENT, 7, '2026-08-02 09:00:00'); expect(app(ClientSmsVolumeCounter::class)->sentThisMonth((int) $this->tenant->id))->toBe(1); Carbon::setTestNow(); });