87 lines
4.2 KiB
PHP
87 lines
4.2 KiB
PHP
|
|
<?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);
|
|||
|
|
});
|