73 lines
3.5 KiB
PHP
73 lines
3.5 KiB
PHP
|
|
<?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);
|
|||
|
|
});
|