110 lines
4.9 KiB
PHP
110 lines
4.9 KiB
PHP
|
|
<?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,
|
||
|
|
]);
|
||
|
|
});
|