Files
portal/app/tests/Feature/Advertising/AdWalletHoldLifecycleTest.php
T

87 lines
4.2 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\AdCampaign;
use App\Models\AdWallet;
use App\Models\AdWalletHold;
use App\Models\Tenant;
use App\Services\Advertising\AdWalletGate;
use App\Services\Advertising\AdWalletService;
use App\Services\Advertising\CampaignImpressionCharger;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(DatabaseTransactions::class);
/** Кампания «за показы» в статусе running с оплаченной сметой. */
function makeHoldLifecycleCampaign(int $tenantId, int $paidImpressions): AdCampaign
{
return AdCampaign::create([
'tenant_id' => $tenantId,
'name' => 'Кампания жизненного цикла заморозки',
'status' => AdCampaign::STATUS_RUNNING,
'audience_days' => 30,
'use_uploaded_list' => true,
'frequency' => 15,
'estimated_impressions' => $paidImpressions,
'paid_impressions' => $paidImpressions,
]);
}
/** Активных холдов у тенанта не осталось. */
function activeHoldCount(int $tenantId): int
{
return AdWalletHold::where('tenant_id', $tenantId)
->where('status', AdWalletHold::STATUS_ACTIVE)->count();
}
/**
* Жизненный цикл заморозки рекламного кошелька.
*
* Заморозка ставится ОДИН раз при запуске кампании на всю смету показов, а
* дальше должна ТАЯТЬ по мере фактических списаний: иначе одни и те же деньги
* считаются дважды (ушли с баланса и остались в заморозке), свободный остаток
* уходит в минус, а AdWalletGate::isSolvent() объявляет клиента неплатёжеспособным
* после первого же суточного списания и AdStopAll глушит все его кампании.
*/
it('melts the active hold while charging so free balance is not double-counted', function () {
$tenant = Tenant::factory()->create();
AdWallet::create(['tenant_id' => $tenant->id, 'balance_rub' => '1000.00', 'frozen_rub' => '0.00']);
$svc = app(AdWalletService::class);
$svc->freeze($tenant->id, 'yandex', 'campaign', 1, '1000.00');
$svc->charge($tenant->id, 'yandex', 'campaign', 1, '200.00', 'yandex-imp:1:100');
$wallet = AdWallet::where('tenant_id', $tenant->id)->first();
// Списанные 200 ₽ ушли с баланса и ОДНОВРЕМЕННО перестали быть зарезервированы.
expect($wallet->balance_rub)->toBe('800.00')
->and($wallet->frozen_rub)->toBe('800.00');
// Свободно = 0 (вся смета ещё занята кампанией), но клиент платёжеспособен —
// кампанию не должно глушить после первого списания.
expect(app(AdWalletGate::class)->isSolvent($tenant->id))->toBeTrue();
// Сам холд тоже похудел — иначе финальный release вернул бы больше, чем занято.
expect(AdWalletHold::where('tenant_id', $tenant->id)->where('source_id', 1)->first()->amount_rub)
->toBe('800.00');
});
// ВЫХОД 1 — кампания открутила смету и помечена «завершена».
it('releases the remaining hold when the campaign completes', function () {
$tenant = Tenant::factory()->create();
$svc = app(AdWalletService::class);
$svc->topup($tenant->id, '10000.00', 'yandex', 'тест');
$campaign = makeHoldLifecycleCampaign($tenant->id, 10000);
// Заморожено с запасом (1500 ₽) над фактической стоимостью сметы (10000 показов × 120 ₽/1000 = 1200 ₽).
$svc->freeze($tenant->id, 'yandex', 'campaign', $campaign->id, '1500.00');
app(CampaignImpressionCharger::class)->charge($campaign, 10000);
$campaign->refresh();
expect($campaign->status)->toBe(AdCampaign::STATUS_COMPLETED)
->and(AdWallet::where('tenant_id', $tenant->id)->first()->frozen_rub)->toBe('0.00')
->and(activeHoldCount($tenant->id))->toBe(0);
});