$tenantId, 'name' => 'МойБренд', 'status' => Sender::STATUS_ACTIVE, 'monthly_fee_rub' => $fee, 'paid_until' => $paidUntil, 'debt_since' => $debtSince, ]); } beforeEach(function () { Carbon::setTestNow('2026-07-27 10:00:00'); config(['client_tg.sandbox' => false]); // деньги включены — проверяем реальный цикл $this->tenant = Tenant::factory()->create(); app(AdWalletService::class)->topup($this->tenant->id, '10000.00', 'telegram', 'test'); }); afterEach(function () { Carbon::setTestNow(); }); // --- Жизненный цикл заявки ------------------------------------------------- it('requestSender создаёт заявку pending и бронирует месячную плату', function () { $sender = tgSenderService()->requestSender($this->tenant->id, 'МойБренд', requestedBy: 1); expect($sender->status)->toBe(Sender::STATUS_PENDING); expect((string) $sender->monthly_fee_rub)->toBe('2500.00'); // из client_tg_settings $wallet = AdWallet::where('tenant_id', $this->tenant->id)->first(); expect((string) $wallet->frozen_rub)->toBe('2500.00'); // бронь под имя }); it('requestSender при активном имени — отказ', function () { makeActiveTgSender($this->tenant->id, '2026-08-27'); expect(fn () => tgSenderService()->requestSender($this->tenant->id, 'Другое')) ->toThrow(DomainException::class); }); it('requestSender при нехватке денег — бросает ДО списания (заявки нет)', function () { $poor = Tenant::factory()->create(); app(AdWalletService::class)->topup($poor->id, '100.00', 'telegram', 'test'); // кошелёк есть, но мало expect(fn () => tgSenderService()->requestSender($poor->id, 'МойБренд')) ->toThrow(InsufficientBalanceException::class); expect(Sender::where('tenant_id', $poor->id)->exists())->toBeFalse(); }); it('disableSender у pending снимает бронь и ставит cancelled', function () { $sender = tgSenderService()->requestSender($this->tenant->id, 'МойБренд'); $out = tgSenderService()->disableSender($this->tenant->id); expect($out->status)->toBe(Sender::STATUS_CANCELLED); $wallet = AdWallet::where('tenant_id', $this->tenant->id)->first(); expect((string) $wallet->frozen_rub)->toBe('0.00'); // бронь снята }); // --- Админ ---------------------------------------------------------------- it('approve списывает плату, снимает бронь, ставит active + paid_until +1мес', function () { $sender = tgSenderService()->requestSender($this->tenant->id, 'МойБренд'); $out = tgSenderService()->approve($sender->id); expect($out->status)->toBe(Sender::STATUS_ACTIVE); expect($out->paid_until->toDateString())->toBe('2026-08-27'); expect($out->debt_since)->toBeNull(); $wallet = AdWallet::where('tenant_id', $this->tenant->id)->first(); expect((string) $wallet->frozen_rub)->toBe('0.00'); // бронь снята expect((string) $wallet->balance_rub)->toBe('7500.00'); // 10000 − 2500 }); it('approve не-pending — отказ', function () { $sender = makeActiveTgSender($this->tenant->id, '2026-08-27'); expect(fn () => tgSenderService()->approve($sender->id)) ->toThrow(DomainException::class); }); it('reject снимает бронь и ставит rejected с заметкой', function () { $sender = tgSenderService()->requestSender($this->tenant->id, 'МойБренд'); $out = tgSenderService()->reject($sender->id, 'Название занято'); expect($out->status)->toBe(Sender::STATUS_REJECTED); expect($out->note)->toBe('Название занято'); $wallet = AdWallet::where('tenant_id', $this->tenant->id)->first(); expect((string) $wallet->frozen_rub)->toBe('0.00'); }); it('disableByAdmin ставит suspended', function () { $sender = makeActiveTgSender($this->tenant->id, '2026-08-27'); $out = tgSenderService()->disableByAdmin($sender->id); expect($out->status)->toBe(Sender::STATUS_SUSPENDED); }); // --- Помесячный джоб ------------------------------------------------------- it('джоб списывает плату, двигает paid_until на месяц, гасит долг когда денег хватает', function () { $sender = makeActiveTgSender($this->tenant->id, '2026-07-26'); // вчера → к оплате app(ChargeTgNameFeeJob::class)->handle(); $sender->refresh(); expect($sender->paid_until->toDateString())->toBe('2026-08-26'); expect($sender->debt_since)->toBeNull(); $wallet = AdWallet::where('tenant_id', $this->tenant->id)->first(); expect((string) $wallet->balance_rub)->toBe('7500.00'); // 10000 − 2500 }); it('джоб при нехватке денег в грейсе — фиксирует долг, paid_until не двигает', function () { $poor = Tenant::factory()->create(); app(AdWalletService::class)->topup($poor->id, '100.00', 'telegram', 'test'); // мало $sender = makeActiveTgSender($poor->id, '2026-07-26'); app(ChargeTgNameFeeJob::class)->handle(); $sender->refresh(); expect($sender->status)->toBe(Sender::STATUS_ACTIVE); // ещё в грейсе expect($sender->paid_until->toDateString())->toBe('2026-07-26'); // не сдвинут expect($sender->debt_since)->not->toBeNull(); }); it('джоб при долге дольше грейса — отключает имя (suspended)', function () { $poor = Tenant::factory()->create(); app(AdWalletService::class)->topup($poor->id, '100.00', 'telegram', 'test'); // долг с 30 дней назад (>29 grace) $sender = makeActiveTgSender($poor->id, '2026-06-26', debtSince: '2026-06-26'); app(ChargeTgNameFeeJob::class)->handle(); $sender->refresh(); expect($sender->status)->toBe(Sender::STATUS_SUSPENDED); }); it('джоб идемпотентен по периоду — второй прогон не списывает дважды', function () { $sender = makeActiveTgSender($this->tenant->id, '2026-07-26'); app(ChargeTgNameFeeJob::class)->handle(); $balanceAfterFirst = (string) AdWallet::where('tenant_id', $this->tenant->id)->first()->balance_rub; // Возвращаем paid_until назад — но external_key периода тот же → повторно не спишет. $sender->update(['paid_until' => '2026-07-26']); app(ChargeTgNameFeeJob::class)->handle(); $chargeCount = AdWalletTransaction::where('tenant_id', $this->tenant->id) ->where('channel', 'telegram') ->where('type', AdWalletTransaction::TYPE_CHARGE) ->count(); expect($chargeCount)->toBe(1); // одно списание за период }); it('джоб в песочнице деньги не двигает', function () { config(['client_tg.sandbox' => true]); $sender = makeActiveTgSender($this->tenant->id, '2026-07-26'); app(ChargeTgNameFeeJob::class)->handle(); $sender->refresh(); expect($sender->paid_until->toDateString())->toBe('2026-07-26'); // не тронут $wallet = AdWallet::where('tenant_id', $this->tenant->id)->first(); expect((string) $wallet->balance_rub)->toBe('10000.00'); // не списано });