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

110 lines
4.9 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\AdWallet;
use App\Models\AdWalletTransaction;
use App\Models\Tenant;
use App\Services\Advertising\AdWalletService;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* Летопись рекламного кошелька должна ОБЪЯСНЯТЬ клиенту каждое движение денег.
*
* Приёмка 03.08.2026 на боевом: у клиента заморожено 3 333,36 ₽, и узнать за что — негде.
* Заморозка писалась в летопись суммой 0,00 ₽ (жёстко зашитой), снятие заморозки не писалось
* вовсе. Деньги при этом не терялись — терялось объяснение, а для клиента, у которого заперта
* треть кошелька, это неотличимо от пропажи.
*
* Сторож держит правило: подвинулась заморозка — в летописи появилась строка с НАСТОЯЩЕЙ
* суммой и указанием, под какую кампанию.
*/
uses(DatabaseTransactions::class);
/** @return array{0: Tenant, 1: AdWalletService} */
function tenantSKoshelkom(string $balance = '10000.00'): array
{
$tenant = Tenant::factory()->create();
AdWallet::create(['tenant_id' => $tenant->id, 'balance_rub' => $balance, 'frozen_rub' => '0.00']);
return [$tenant, app(AdWalletService::class)];
}
/** @return Collection<int, AdWalletTransaction> */
function letopis(int $tenantId, string $type)
{
return AdWalletTransaction::where('tenant_id', $tenantId)->where('type', $type)->get();
}
it('заморозка записана настоящей суммой, а не нулём', function () {
[$tenant, $svc] = tenantSKoshelkom();
$svc->freeze($tenant->id, 'yandex', 'campaign', 6, '3333.36');
$rows = letopis($tenant->id, AdWalletTransaction::TYPE_FREEZE);
expect($rows)->toHaveCount(1);
$row = $rows->first();
// Знак как у списания: деньги ушли из свободных. Сам баланс заморозка не двигает.
expect((string) $row->amount_rub)->toBe('-3333.36')
->and((string) $row->balance_rub_after)->toBe('10000.00')
->and($row->channel)->toBe('yandex')
->and($row->related_type)->toBe('campaign')
->and($row->related_id)->toBe(6)
// Клиент должен понять, ПОД ЧТО заперты деньги, не выходя из кошелька.
->and($row->description)->toContain('6');
});
it('снятие заморозки записано отдельной строкой с суммой', function () {
[$tenant, $svc] = tenantSKoshelkom();
$svc->freeze($tenant->id, 'yandex', 'campaign', 6, '3333.36');
$svc->release($tenant->id, 'yandex', 'campaign', 6);
$rows = letopis($tenant->id, AdWalletTransaction::TYPE_RELEASE);
expect($rows)->toHaveCount(1);
$row = $rows->first();
expect((string) $row->amount_rub)->toBe('3333.36')
->and((string) $row->balance_rub_after)->toBe('10000.00')
->and($row->related_id)->toBe(6)
->and($row->description)->toContain('6');
});
it('повторная заморозка той же кампании не плодит вторую строку', function () {
[$tenant, $svc] = tenantSKoshelkom();
$svc->freeze($tenant->id, 'yandex', 'campaign', 6, '3333.36');
$svc->freeze($tenant->id, 'yandex', 'campaign', 6, '3333.36'); // идемпотентность
expect(letopis($tenant->id, AdWalletTransaction::TYPE_FREEZE))->toHaveCount(1);
});
it('снятие несуществующей заморозки летопись не трогает', function () {
[$tenant, $svc] = tenantSKoshelkom();
$svc->release($tenant->id, 'yandex', 'campaign', 777);
expect(letopis($tenant->id, AdWalletTransaction::TYPE_RELEASE))->toHaveCount(0);
});
it('пауза и возобновление оставляют читаемую пару строк', function () {
[$tenant, $svc] = tenantSKoshelkom();
$svc->freeze($tenant->id, 'yandex', 'campaign', 6, '3333.36');
$svc->release($tenant->id, 'yandex', 'campaign', 6); // пауза
$svc->freeze($tenant->id, 'yandex', 'campaign', 6, '3333.36'); // возобновление
// Три события — три строки: заморозили, сняли, заморозили заново.
$vse = AdWalletTransaction::where('tenant_id', $tenant->id)
->whereIn('type', [AdWalletTransaction::TYPE_FREEZE, AdWalletTransaction::TYPE_RELEASE])
->orderBy('id')->get();
expect($vse->pluck('type')->all())->toBe([
AdWalletTransaction::TYPE_FREEZE,
AdWalletTransaction::TYPE_RELEASE,
AdWalletTransaction::TYPE_FREEZE,
]);
});