Files
portal/app/tests/Feature/ClientSms/NameFeeJobTest.php
T
Дмитрий 51b8f501ae feat(смс-клиент): бэкенд имени и авто-рассылки
Жизненный цикл имени: клиент requestSender (freeze первого месяца) / disableSender;
админ approve (charge+release, active, paid_until+1мес) / reject / disable. Помесячная
оплата ChargeSmsNameFeeJob (проверка средств ДО списания, долг>29 дней→suspended,
идемпотентно по external_key), расписание в console.php. Авто-рассылка: DealSmsObserver
(защитный, freshness-guard, НИКОГДА не роняет приём лида) → SendAutoSmsForDealJob
(best-effort, идемпотентно по deal_id, песочница/деньги/маршрут). Действующее имя
кампании = active-имя тенанта иначе liderra.ru.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 23:20:52 +03:00

162 lines
6.7 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\Jobs\ChargeSmsNameFeeJob;
use App\Models\AdWallet;
use App\Models\AdWalletTransaction;
use App\Models\ClientSmsSender;
use App\Models\Tenant;
use App\Services\Advertising\AdWalletService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Tests\Concerns\SharesSupplierPdo;
/**
* Task 14: ежемесячная плата за именного отправителя (client_sms_senders).
*
* Джоб перечисляет активных отправителей КРОСС-ТЕНАНТНО через pgsql_supplier
* (BYPASSRLS) — без share PDO вставленные в тест-транзакции отправители
* (default pgsql-соединение) не видны второму соединению до commit'а. Поэтому
* подключаем RefreshDatabase + SharesSupplierPdo вместе (зеркалит
* tests/Feature/Advertising/ChargeCampaignSpendJobTest).
*
* Настройки (fee 2500.00, grace 29) сидированы миграцией client_sms_settings.
*/
uses(RefreshDatabase::class);
uses(SharesSupplierPdo::class);
function makeActiveSender(int $tenantId, ?string $paidUntil, string $fee = '100.00', ?string $debtSince = null): ClientSmsSender
{
return ClientSmsSender::create([
'tenant_id' => $tenantId,
'name' => 'MyBrand',
'name_type' => ClientSmsSender::TYPE_COMPANY,
'status' => ClientSmsSender::STATUS_ACTIVE,
'operators' => ['mts'],
'monthly_fee_rub' => $fee,
'paid_until' => $paidUntil,
'debt_since' => $debtSince,
]);
}
beforeEach(function () {
Carbon::setTestNow('2026-07-25 10:00:00');
config(['services.sms.sandbox' => false]);
});
afterEach(function () {
Carbon::setTestNow();
});
it('charges the monthly fee, advances paid_until ~1 month, clears debt when funded', function () {
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '5000.00', null, 'test');
$sender = makeActiveSender($tenant->id, '2026-07-24'); // вчера → к оплате
app(ChargeSmsNameFeeJob::class)->handle();
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect((string) $wallet->balance_rub)->toBe('4900.00'); // 5000 100
$sender->refresh();
expect($sender->status)->toBe(ClientSmsSender::STATUS_ACTIVE)
->and($sender->paid_until->toDateString())->toBe('2026-08-24')
->and($sender->debt_since)->toBeNull();
$tx = AdWalletTransaction::where('tenant_id', $tenant->id)
->where('external_key', "sms:sender:{$sender->id}:2026-07")
->first();
expect($tx)->not->toBeNull()
->and($tx->type)->toBe(AdWalletTransaction::TYPE_CHARGE)
->and($tx->amount_rub)->toBe('-100.00');
});
it('sets debt_since to today and keeps the sender active when funds are short', function () {
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '50.00', null, 'test'); // < fee 100
$sender = makeActiveSender($tenant->id, '2026-07-24');
app(ChargeSmsNameFeeJob::class)->handle();
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect((string) $wallet->balance_rub)->toBe('50.00'); // не тронут
$sender->refresh();
expect($sender->status)->toBe(ClientSmsSender::STATUS_ACTIVE)
->and($sender->debt_since->toDateString())->toBe('2026-07-25')
->and($sender->paid_until->toDateString())->toBe('2026-07-24'); // НЕ продлён
expect(AdWalletTransaction::where('tenant_id', $tenant->id)
->where('type', AdWalletTransaction::TYPE_CHARGE)->count())->toBe(0);
});
it('suspends the sender when the debt is older than the grace period', function () {
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '50.00', null, 'test'); // всё ещё нет денег
// grace 29; долг с 2026-06-25 = 30 дней назад → 30 > 29 → отключение.
$sender = makeActiveSender($tenant->id, '2026-07-24', '100.00', '2026-06-25');
app(ChargeSmsNameFeeJob::class)->handle();
$sender->refresh();
expect($sender->status)->toBe(ClientSmsSender::STATUS_SUSPENDED)
->and($sender->note)->toContain('Долг более 29 дней')
->and($sender->debt_since->toDateString())->toBe('2026-06-25')
->and($sender->paid_until->toDateString())->toBe('2026-07-24'); // НЕ продлён
});
it('leaves a sender untouched when paid_until is still in the future', function () {
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '5000.00', null, 'test');
$sender = makeActiveSender($tenant->id, '2026-07-26'); // завтра → не к оплате
app(ChargeSmsNameFeeJob::class)->handle();
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect((string) $wallet->balance_rub)->toBe('5000.00');
$sender->refresh();
expect($sender->status)->toBe(ClientSmsSender::STATUS_ACTIVE)
->and($sender->paid_until->toDateString())->toBe('2026-07-26')
->and($sender->debt_since)->toBeNull();
expect(AdWalletTransaction::where('tenant_id', $tenant->id)
->where('type', AdWalletTransaction::TYPE_CHARGE)->count())->toBe(0);
});
it('does not double-charge when the whole job runs twice on the same day', function () {
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '5000.00', null, 'test');
$sender = makeActiveSender($tenant->id, '2026-07-24');
app(ChargeSmsNameFeeJob::class)->handle();
$balanceAfterFirst = (string) AdWallet::where('tenant_id', $tenant->id)->first()->balance_rub;
app(ChargeSmsNameFeeJob::class)->handle();
$balanceAfterSecond = (string) AdWallet::where('tenant_id', $tenant->id)->first()->balance_rub;
expect($balanceAfterFirst)->toBe('4900.00')
->and($balanceAfterSecond)->toBe('4900.00'); // без повторного списания
expect(AdWalletTransaction::where('tenant_id', $tenant->id)
->where('type', AdWalletTransaction::TYPE_CHARGE)->count())->toBe(1);
});
it('is a no-op in sandbox mode (no money moves)', function () {
config(['services.sms.sandbox' => true]);
$tenant = Tenant::factory()->create();
app(AdWalletService::class)->topup($tenant->id, '5000.00', null, 'test');
$sender = makeActiveSender($tenant->id, '2026-07-24');
app(ChargeSmsNameFeeJob::class)->handle();
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
expect((string) $wallet->balance_rub)->toBe('5000.00');
$sender->refresh();
expect($sender->status)->toBe(ClientSmsSender::STATUS_ACTIVE)
->and($sender->paid_until->toDateString())->toBe('2026-07-24');
});