a709efdee4
Владелец 06.08: "кошелёк убогий и неинформативный". Отметил пять слабых мест из шести. Не отметил только прогноз "надолго ли хватит" - он пополняет по факту, а не по остатку. Что теперь на экране: - слева липкая колонка денег: свободно крупно, и под ней РАСШИФРОВКА заморозки - какая кампания, сколько заперто, когда вернётся. Раньше стояло голое "Заморожено 2 507 руб" без единого слова, под что; - период: сегодня / вчера / 7 дней / 30 дней / свои даты, живёт в адресе страницы; - столбики расхода по дням, цвет по каналу, своим CSS без библиотеки; - таблица "Куда ушли" - строка на кампанию с суммой и долей, клик отбирает ленту; - закладки по каналам сохранены, но теперь считаются за выбранный период; - лента разбита по дням с итогом за день и постраничной догрузкой. Сервер: две новые сборки RaskryitieZamorozki и OtchyotKoshelka, новая ручка /api/advertising/wallet/report, лента получила отбор по датам, каналу и кампании и листание по ключу вместо смещения. Расчёт списаний, заморозки и возврата по сроку НЕ тронут - правка только про показ. Найдено по дороге и закрыто: - чтение отчёта обёрнуто контекстом построчной защиты. Без этого на боевом запрос вернул бы НОЛЬ строк молча: местная база под суперпользователем защиту обходит, и мы бы увидели это только у клиента; - сторож на число запросов мог зеленеть БЕЗ самой ручки - на любой неизвестный путь портал отвечает страницей с кодом 200. Усилен и проверен вырезанием маршрута; - общий форматтер срезал хвостовой ноль: "833,50" превращалось в "833,5". На это независимо наступили три правки подряд, каждая завела свою копию. Сведено в formatExact. Границы суток и группировка по дням считаются ПО МОСКВЕ, а не по Гринвичу. Сторож взят с живого боевого: запись 69, 2026-08-05 21:00:24 UTC - для человека это 6 августа. Наивные версии всех трёх мест были написаны нарочно и увидены красными, прежде чем чинились. Старые сторожа не выброшены, а перенесены: закладки по каналам переехали в отчёт вместе с проверкой на 120 строк, отбор ленты по каналу - в проверку ленты. Каждый переезд назван поимённо. Проверено: 461 сторож сервера, 2135 сторожей экрана, контроль типов ноль, статанализ ноль, форматтер чисто, сборка фронта проходит. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
155 lines
6.6 KiB
PHP
155 lines
6.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\AdCampaign;
|
|
use App\Models\AdWallet;
|
|
use App\Models\AdWalletTransaction;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\Advertising\AdWalletService;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
|
|
/**
|
|
* Постраничная лента движений кошелька (Задача 5): отбор по датам/каналу/кампании,
|
|
* листание по ключу `before_id`, страница 50 строк.
|
|
*
|
|
* 🔴 Закладки по каналам и итог трат отсюда УЕХАЛИ в отчёт (`/wallet/report`) —
|
|
* см. KoshelyokOtchyotZaPeriodTest.php. Здесь проверяется только сама лента.
|
|
*/
|
|
uses(DatabaseTransactions::class);
|
|
|
|
beforeEach(function () {
|
|
$this->tenant = Tenant::factory()->create();
|
|
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
|
|
$this->actingAs($this->user);
|
|
AdWallet::create(['tenant_id' => $this->tenant->id, 'balance_rub' => '0.00', 'frozen_rub' => '0.00']);
|
|
$this->svc = app(AdWalletService::class);
|
|
$this->svc->topup($this->tenant->id, '100000.00', 'yandex', 'Пополнение по счёту №12');
|
|
// Пополнение из beforeEach иначе пишется с created_at «сейчас» и в тестах, что фильтруют
|
|
// ленту по конкретным суткам, может случайно попасть в проверяемое окно — тест не должен
|
|
// зависеть от того, какое сегодня число на календаре.
|
|
AdWalletTransaction::where('tenant_id', $this->tenant->id)->where('type', 'topup')
|
|
->update(['created_at' => '2020-01-01 00:00:00']);
|
|
});
|
|
|
|
it('отдаёт страницу в 50 строк и говорит, что есть ещё', function () {
|
|
$k = AdCampaign::create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'name' => 'Долгая',
|
|
'audience_days' => 10,
|
|
'status' => AdCampaign::STATUS_RUNNING,
|
|
'estimated_impressions' => 1000,
|
|
'paid_impressions' => 1000,
|
|
'charged_client_rub' => '0.00',
|
|
]);
|
|
for ($i = 0; $i < 60; $i++) {
|
|
$this->svc->charge($this->tenant->id, 'yandex', 'campaign', $k->id, '1.00', 'k'.$i);
|
|
}
|
|
|
|
$body = $this->getJson('/api/advertising/wallet/transactions')
|
|
->assertOk()->assertJsonStructure(['items', 'has_more'])->json();
|
|
|
|
expect($body['items'])->toHaveCount(50)
|
|
->and($body['has_more'])->toBeTrue()
|
|
->and($body['next_before_id'])->toBe($body['items'][49]['id']);
|
|
});
|
|
|
|
it('«показать ещё» не задваивает и не теряет строки', function () {
|
|
$k = AdCampaign::create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'name' => 'Долгая',
|
|
'audience_days' => 10,
|
|
'status' => AdCampaign::STATUS_RUNNING,
|
|
'estimated_impressions' => 1000,
|
|
'paid_impressions' => 1000,
|
|
'charged_client_rub' => '0.00',
|
|
]);
|
|
for ($i = 0; $i < 60; $i++) {
|
|
$this->svc->charge($this->tenant->id, 'yandex', 'campaign', $k->id, '1.00', 'k'.$i);
|
|
}
|
|
|
|
$pervaya = $this->getJson('/api/advertising/wallet/transactions')->assertOk()->json();
|
|
$vtoraya = $this->getJson('/api/advertising/wallet/transactions?before_id='.$pervaya['next_before_id'])
|
|
->assertOk()->json();
|
|
|
|
$ids = array_merge(
|
|
array_column($pervaya['items'], 'id'),
|
|
array_column($vtoraya['items'], 'id'),
|
|
);
|
|
|
|
expect(count($ids))->toBe(count(array_unique($ids)))
|
|
->and($vtoraya['has_more'])->toBeFalse();
|
|
});
|
|
|
|
it('отбирает ленту по кампании', function () {
|
|
$odna = AdCampaign::create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'name' => 'Первая',
|
|
'audience_days' => 10,
|
|
'status' => AdCampaign::STATUS_RUNNING,
|
|
'estimated_impressions' => 1000,
|
|
'paid_impressions' => 1000,
|
|
'charged_client_rub' => '0.00',
|
|
]);
|
|
$drugaya = AdCampaign::create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'name' => 'Вторая',
|
|
'audience_days' => 10,
|
|
'status' => AdCampaign::STATUS_RUNNING,
|
|
'estimated_impressions' => 1000,
|
|
'paid_impressions' => 1000,
|
|
'charged_client_rub' => '0.00',
|
|
]);
|
|
$this->svc->charge($this->tenant->id, 'yandex', 'campaign', $odna->id, '10.00', 'a');
|
|
$this->svc->charge($this->tenant->id, 'yandex', 'campaign', $drugaya->id, '20.00', 'b');
|
|
|
|
$body = $this->getJson('/api/advertising/wallet/transactions'
|
|
.'?source_type=campaign&source_id='.$odna->id.'&channel=yandex')->assertOk()->json();
|
|
|
|
expect($body['items'])->toHaveCount(1)
|
|
->and($body['items'][0]['description'])->toContain('«Первая»');
|
|
});
|
|
|
|
it('отбирает ленту по каналу', function () {
|
|
$k = AdCampaign::create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'name' => 'Смешанная',
|
|
'audience_days' => 10,
|
|
'status' => AdCampaign::STATUS_RUNNING,
|
|
'estimated_impressions' => 1000,
|
|
'paid_impressions' => 1000,
|
|
'charged_client_rub' => '0.00',
|
|
]);
|
|
$this->svc->charge($this->tenant->id, 'yandex', 'campaign', $k->id, '5.00', 'y:1');
|
|
$this->svc->charge($this->tenant->id, 'sms', 'campaign', $k->id, '7.00', 's:1');
|
|
|
|
$items = $this->getJson('/api/advertising/wallet/transactions?channel=sms')->assertOk()->json('items');
|
|
|
|
expect($items)->toHaveCount(1)
|
|
->and($items[0]['channel'])->toBe('sms')
|
|
->and($items[0]['amount_rub'])->toBe('-7.00');
|
|
});
|
|
|
|
it('отбирает ленту по московским суткам', function () {
|
|
$k = AdCampaign::create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'name' => 'Ночная',
|
|
'audience_days' => 10,
|
|
'status' => AdCampaign::STATUS_RUNNING,
|
|
'estimated_impressions' => 1000,
|
|
'paid_impressions' => 1000,
|
|
'charged_client_rub' => '0.00',
|
|
]);
|
|
$this->svc->charge($this->tenant->id, 'yandex', 'campaign', $k->id, '9.00', 'n');
|
|
AdWalletTransaction::where('tenant_id', $this->tenant->id)
|
|
->where('type', 'charge')->orderByDesc('id')->limit(1)
|
|
->update(['created_at' => '2026-08-05 21:00:24']);
|
|
|
|
// 21:00 UTC 5-го = 00:00 МСК 6-го: в 5 августа не попадает, в 6-е попадает.
|
|
expect($this->getJson('/api/advertising/wallet/transactions?from=2026-08-05&to=2026-08-05')
|
|
->assertOk()->json('items'))->toHaveCount(0);
|
|
expect($this->getJson('/api/advertising/wallet/transactions?from=2026-08-06&to=2026-08-06')
|
|
->assertOk()->json('items'))->toHaveCount(1);
|
|
});
|