Files
portal/app/tests/Feature/ClientSms/NameFeeJobTest.php
T

178 lines
7.6 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Jobs\ChargeSmsNameFeeJob;
use App\Models\AdWallet;
use App\Models\AdWalletTransaction;
use App\Models\ClientSmsSender;
use App\Models\ClientSmsSettings;
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('не падает целиком при пустой таблице настроек — берёт grace по умолчанию', function () {
ClientSmsSettings::query()->delete(); // настроек нет — джоб не должен упасть до обработки
$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(); // не должно бросить
$sender->refresh();
expect($sender->status)->toBe(ClientSmsSender::STATUS_ACTIVE)
->and($sender->paid_until->toDateString())->toBe('2026-08-24')
->and((string) AdWallet::where('tenant_id', $tenant->id)->first()->balance_rub)->toBe('4900.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');
});