$tenantId, 'enabled' => true, 'ad_text' => 'Заходите в наш канал', 'ad_link' => 'https://t.me/example_channel', 'ord_category' => 'Размещение рекламы', 'budget_cap_rub' => '2500.00', 'daily_limit_rub' => $dailyLimit, ]); } /** @return list */ function safeguardPhones(int $n): array { $out = []; for ($i = 1; $i <= $n; $i++) { $out[] = '7999'.str_pad((string) $i, 7, '0', STR_PAD_LEFT); } return $out; } function feed(int $tenantId, int $n): void { foreach (safeguardPhones($n) as $phone) { app(TelegramAutoAccumulator::class)->accumulate($tenantId, $phone); } } /** Сколько сейчас на общем балансе тенанта. */ function tenantBalance(int $tenantId): string { return (string) Tenant::whereKey($tenantId)->value('balance_rub'); } beforeEach(function () { Carbon::setTestNow('2026-07-28 10:00:00'); Queue::fake(); config(['client_tg.auto_batch_threshold' => 3]); // порог занижен для теста config(['client_tg.sandbox' => false]); // деньги включены — проверяем боевой гейт $this->tenant = Tenant::factory()->create(['balance_rub' => '0.00']); DB::statement('SET LOCAL app.current_tenant_id = '.$this->tenant->id); // Клиентская смета пачки из 3 кандидатов = базовая смета × наценка (то, что списывается). $this->cost = app(TelegramTariffService::class)->clientEstimateRub(3); }); afterEach(function () { Carbon::setTestNow(); }); it('в бою при нехватке денег на балансе не ставит в очередь и пишет причину', function () { $this->tenant->update(['balance_rub' => '1.00']); // заведомо меньше клиентской сметы пачки expect(bccomp('1.00', $this->cost, 2))->toBeLessThan(0); // подстраховка: денег правда мало safeguardRule($this->tenant->id, dailyLimit: '1000.00'); Log::spy(); feed($this->tenant->id, 3); Queue::assertNothingPushed(); $campaign = Campaign::where('tenant_id', $this->tenant->id)->first(); expect($campaign->status)->toBe(Campaign::STATUS_DRAFT); // остаётся черновиком expect(tenantBalance($this->tenant->id))->toBe('1.00'); // баланс не тронут expect(BalanceTransaction::where('type', BalanceTransaction::TYPE_TG_AD_CHARGE) ->where('related_id', $campaign->id)->count())->toBe(0); // списания не было Log::shouldHaveReceived('info')->withArgs(fn ($msg) => $msg === 'client_tg.auto_skipped')->atLeast()->once(); }); it('в бою при исчерпанном дневном лимите не ставит в очередь (cost = клиентская смета)', function () { $this->tenant->update(['balance_rub' => '10000.00']); // денег вдоволь safeguardRule($this->tenant->id, dailyLimit: '0.01'); // лимит заведомо < клиентской сметы feed($this->tenant->id, 3); Queue::assertNothingPushed(); $campaign = Campaign::where('tenant_id', $this->tenant->id)->first(); expect($campaign->status)->toBe(Campaign::STATUS_DRAFT); expect(tenantBalance($this->tenant->id))->toBe('10000.00'); // упёрлись в лимит ДО списания expect(BalanceTransaction::where('type', BalanceTransaction::TYPE_TG_AD_CHARGE) ->where('related_id', $campaign->id)->count())->toBe(0); }); it('дневной лимит по умолчанию 0 → авто выключено даже при деньгах', function () { $this->tenant->update(['balance_rub' => '10000.00']); // daily_limit_rub не задан явно → дефолт 0.00 AutoRule::create([ 'tenant_id' => $this->tenant->id, 'enabled' => true, 'ad_text' => 'Текст', 'ad_link' => 'https://t.me/x', 'ord_category' => 'Размещение рекламы', 'budget_cap_rub' => '2500.00', ]); feed($this->tenant->id, 3); Queue::assertNothingPushed(); expect(tenantBalance($this->tenant->id))->toBe('10000.00'); // ничего не списано }); it('в бою при деньгах и лимите в норме — списывает смету с баланса, ставит в очередь и учитывает трату за день', function () { $this->tenant->update(['balance_rub' => '10000.00']); $rule = safeguardRule($this->tenant->id, dailyLimit: '1000.00'); feed($this->tenant->id, 3); Queue::assertPushed(RunTelegramCampaignJob::class, 1); $campaign = Campaign::where('tenant_id', $this->tenant->id)->first(); expect($campaign->status)->toBe(Campaign::STATUS_QUEUED); // Баланс упал ровно на клиентскую смету, проводка списания одна. expect(tenantBalance($this->tenant->id))->toBe(bcsub('10000.00', $this->cost, 2)); expect(BalanceTransaction::where('type', BalanceTransaction::TYPE_TG_AD_CHARGE) ->where('related_id', $campaign->id)->count())->toBe(1); $rule->refresh(); expect((string) $rule->spent_today_rub)->toBe($this->cost); // смета записана в трату за день expect($rule->spent_date->toDateString())->toBe('2026-07-28'); }); it('счётчик за день сбрасывается на новую дату', function () { $this->tenant->update(['balance_rub' => '10000.00']); $rule = safeguardRule($this->tenant->id, dailyLimit: $this->cost); // Вчерашняя трата «под завязку» — но сегодня лимит должен освободиться. $rule->update(['spent_today_rub' => $this->cost, 'spent_date' => '2026-07-27']); feed($this->tenant->id, 3); Queue::assertPushed(RunTelegramCampaignJob::class, 1); $rule->refresh(); expect((string) $rule->spent_today_rub)->toBe($this->cost); // счётчик пересчитан с нуля за сегодня }); it('в песочнице предохранитель не вмешивается — ставит в очередь без денег', function () { config(['client_tg.sandbox' => true]); // Баланс нулевой (из beforeEach), лимита нет — но песочница деньги не трогает. safeguardRule($this->tenant->id, dailyLimit: '0.00'); feed($this->tenant->id, 3); Queue::assertPushed(RunTelegramCampaignJob::class, 1); expect(tenantBalance($this->tenant->id))->toBe('0.00'); // деньги не тронуты expect(BalanceTransaction::where('type', BalanceTransaction::TYPE_TG_AD_CHARGE)->count())->toBe(0); });