Files
portal/app/tests/Feature/ClientTg/WalletChannelTest.php
T
Дмитрий 298afb835a feat(телеграм-модуль): ядро клиентского модуля Telegram-рекламы — таблицы, модели, цена, аудитория, кошелёк
Сессия 1 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Бэкенд-ядро — зеркало готового СМС-модуля. Робота ещё нет — он в Сессии 2.

- 8 таблиц client_tg_* с RLS tenant_isolation и GRANT crm_app_user; справочные
  tariffs/settings без RLS с guarded-GRANT. rls-reviewer PASS 8 из 8.
- 7 моделей ClientTg + связи campaign->phones.
- Ступенчатая цена TelegramTariffService — ₽ за показ по объёму; тариф = потолок,
  точную стоимость считает МТС.
- Сборка аудитории TelegramAudienceService — сделки за период / своя база / свой
  список, нормализация телефона, стоп-лист и дедуп; отдаёт кандидатов, реальный
  охват узнаёт робот после загрузки в МТС.
- Канал кошелька telegram: перенесён общий AdWallet со свежей версии портала
  — модели, сервис, миграции; списание и заморозка по каналу telegram под тестами,
  charge не бросает и не уходит в минус, нехватка средств ловится freeze до списания.

Проверки: 26 из 26 Pest зелёные, larastan 0, gitleaks чисто.
db/CHANGELOG_schema.md v8.86 — номер предварительный, ветка отстаёт от main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 16:15:17 +03:00

73 lines
3.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Exceptions\Billing\InsufficientBalanceException;
use App\Models\AdWallet;
use App\Models\AdWalletTransaction;
use App\Models\Tenant;
use App\Services\Advertising\AdWalletService;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
const TG_CHANNEL = 'telegram';
it('замораживает резерв под Telegram-кампанию и учитывает его по каналу', function () {
$tenant = Tenant::factory()->create();
$svc = app(AdWalletService::class);
$svc->topup($tenant->id, '1000.00', TG_CHANNEL, 'Пополнение');
$svc->freeze($tenant->id, TG_CHANNEL, 'client_tg_campaign', 5, '420.00');
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect((string) $wallet->balance_rub)->toBe('1000.00')
->and((string) $wallet->frozen_rub)->toBe('420.00')
->and(AdWalletTransaction::where('tenant_id', $tenant->id)->where('channel', TG_CHANNEL)->count())->toBe(2); // topup + freeze
});
it('freeze стережёт нехватку средств и бросает ДО списания', function () {
$tenant = Tenant::factory()->create();
$svc = app(AdWalletService::class);
$svc->topup($tenant->id, '300.00', TG_CHANNEL, 'Пополнение');
// free = balance frozen = 300 < 400 → бросок ДО каких-либо списаний
expect(fn () => $svc->freeze($tenant->id, TG_CHANNEL, 'client_tg_campaign', 1, '400.00'))
->toThrow(InsufficientBalanceException::class);
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect((string) $wallet->frozen_rub)->toBe('0.00'); // ничего не заморозилось
});
it('charge НЕ бросает и обнуляет баланс, не уходя в минус', function () {
$tenant = Tenant::factory()->create();
$svc = app(AdWalletService::class);
$svc->topup($tenant->id, '300.00', TG_CHANNEL, 'Пополнение');
// charge не бросает — потому проверка средств обязана быть ДО (через freeze).
$svc->charge($tenant->id, TG_CHANNEL, 'client_tg_campaign', 1, '400.00', 'tg:1:test');
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect((string) $wallet->balance_rub)->toBe('0.00') // пол по нулю, не минус
->and(AdWalletTransaction::where('tenant_id', $tenant->id)
->where('channel', TG_CHANNEL)->where('type', AdWalletTransaction::TYPE_CHARGE)->exists())->toBeTrue();
});
it('полный поток telegram: topup → freeze → charge, идемпотентно по external_key', function () {
$tenant = Tenant::factory()->create();
$svc = app(AdWalletService::class);
$svc->topup($tenant->id, '1000.00', TG_CHANNEL, 'Пополнение');
$svc->freeze($tenant->id, TG_CHANNEL, 'client_tg_campaign', 7, '420.00');
$svc->charge($tenant->id, TG_CHANNEL, 'client_tg_campaign', 7, '420.00', 'tg:7:2026-07-27');
// повторное списание тем же external_key — идемпотентно, ничего не меняет
$svc->charge($tenant->id, TG_CHANNEL, 'client_tg_campaign', 7, '420.00', 'tg:7:2026-07-27');
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect((string) $wallet->balance_rub)->toBe('580.00')
->and(AdWalletTransaction::where('tenant_id', $tenant->id)
->where('channel', TG_CHANNEL)->where('type', AdWalletTransaction::TYPE_CHARGE)->count())->toBe(1);
});