1ed82b6b36
Продолжение правки отдела продаж от 02.08: тот же класс ошибки найден ещё
в тринадцати местах. Граница периода, посчитанная по Москве, уходит в базу
надписью без смещения и читается как гринвичская — сутки съезжают на три часа.
У ошибки оказалось ДВА вкуса, и поиск из промта находил только первый:
1. граница по Москве, отданная как есть (дашборд клиента, списания,
расход на рекламу, карточка клиента у отдела продаж, сводка админа);
2. граница ВООБЩЕ без Москвы - now()->startOfDay() и Carbon::today();
пояс приложения гринвичский, значит день начинался в 03:00 МСК
(список лидов и выгрузка, биллинг, карточка клиента у админа,
посетители, ответ бота клиенту, сверка CSV, снимок рекламной кампании).
Правило названо в одном месте - App\Support\MskBoundary: instant() отдаёт
границу мгновением, dayAfter() - первое мгновение после дня (полуинтервал,
чтобы не терять последнюю секунду). Календарные даты и счёт дней НЕ трогали:
там нужен именно московский календарь.
Заодно в списаниях клиента убрана вторая копия условий периода - выгрузка
CSV фильтровала по своей копии, и та уже разъехалась с общей.
Проверка: tests/Feature/NightBoundaryMskTest.php - девять проверок с часами,
замороженными на 00:30 МСК. До правки девять из девяти красные, после - зелёные.
Две из них поначалу проходили и на сломанном коде: в 00:30 МСК гринвичский день
это ещё вчерашний, и ночное событие случайно попадало в окно; добавлено второе
событие "вчера днём", которое обязано остаться за бортом.
Три чужие проверки закрепляли старую ошибку и поправлены:
- DealIndexTest "конец дня" клал заявку в 23:30 по Гринвичу - по московскому
календарю это уже 02:30 следующего дня;
- ClientFactsTest и GuardCardContractTest строят заявки от now() и ночью
краснеют сами - часы заморожены на 12:00 МСК с возвратом в afterEach.
Статанализ: 0. Форматтер: чисто.
127 lines
6.5 KiB
PHP
127 lines
6.5 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
/**
|
||
* СВЯЗКА между генератором карточки (ClientFacts) и сторожом (AnswerGuard).
|
||
*
|
||
* AnswerGuardTest.php ловит перепутанные цифры на карточке, написанной ВРУЧНУЮ в самом
|
||
* тесте (trapCard()) — она устроена так, как сторож ЕЁ понимает, но не проверяет, что
|
||
* сторож понимает НАСТОЯЩУЮ карточку, которую строит ClientFacts::card() на реальных
|
||
* данных. Если кто-то поменяет формулировку в ClientFacts («Баланс:» → «Ваш баланс:»),
|
||
* сторож молча перестанет находить факты — и пропустит враньё про цифры клиента.
|
||
* Ручной тест этого не заметит, а этот — обязан.
|
||
*
|
||
* 🔴 Если этот тест УПАЛ после правки текста в ClientFacts — это значит сторож ОСЛЕП,
|
||
* а не что тест «слишком строгий». Чинить нужно разбор в AnswerGuard::parseCardFacts()
|
||
* (синхронно с новой формулировкой), а НЕ подгонять этот тест под новый текст.
|
||
*/
|
||
|
||
use App\Models\Deal;
|
||
use App\Models\PricingTier;
|
||
use App\Models\Project;
|
||
use App\Models\Tenant;
|
||
use App\Models\User;
|
||
use App\Services\Bot\AnswerGuard;
|
||
use App\Services\Bot\ClientFacts;
|
||
use Carbon\Carbon;
|
||
use Carbon\CarbonImmutable;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
|
||
uses(RefreshDatabase::class);
|
||
|
||
// Часы держим на полудне по Москве — как в ClientFactsTest. Карточка считает
|
||
// «сегодня» по московским суткам, а заявки здесь кладутся от `now()`; ночью,
|
||
// с 00:00 до 03:00 МСК, эти два дня расходятся и проверка краснела бы.
|
||
beforeEach(function (): void {
|
||
$polden = CarbonImmutable::now('Europe/Moscow')->startOfDay()->addHours(12);
|
||
Carbon::setTestNow($polden);
|
||
CarbonImmutable::setTestNow($polden);
|
||
});
|
||
|
||
afterEach(function (): void {
|
||
Carbon::setTestNow();
|
||
CarbonImmutable::setTestNow();
|
||
});
|
||
|
||
/**
|
||
* Те же две ступени, что в ClientFactsTest::tiers() — имя своё (guardContract*), чтобы
|
||
* не столкнуться с глобальной функцией `tiers()` из ClientFactsTest.php при общем прогоне.
|
||
*/
|
||
function guardContractPricingTiers(): void
|
||
{
|
||
PricingTier::factory()->create(['tier_no' => 1, 'leads_in_tier' => 300, 'price_per_lead_kopecks' => 5000]);
|
||
PricingTier::factory()->create(['tier_no' => 2, 'leads_in_tier' => null, 'price_per_lead_kopecks' => 4000]);
|
||
}
|
||
|
||
/**
|
||
* Строит НАСТОЯЩУЮ карточку через ClientFacts::card() на нарочно подобранных данных:
|
||
* баланс 2 000 ₽ по 50 ₽/заявку (1-я ступень) = РОВНО 40 заявок — число нарочно совпадает
|
||
* с «получено в этом месяце» (12) не будучи им, чтобы ловушка «перепутанные местами
|
||
* цифры» была видна: 40 в карточке РЕАЛЬНО есть (баланс), но не как «получено в этом
|
||
* месяце»; 12 — другое число (месяц), не «сегодня»; 3 — третье число (сегодня).
|
||
*/
|
||
function guardContractRealCard(): string
|
||
{
|
||
guardContractPricingTiers();
|
||
|
||
$tenant = Tenant::factory()->create(['balance_rub' => '2000.00', 'delivered_in_month' => 12]);
|
||
|
||
$project = Project::factory()->create([
|
||
'tenant_id' => $tenant->id, 'is_active' => true, 'daily_limit_target' => 10,
|
||
]);
|
||
Project::factory()->create([
|
||
'tenant_id' => $tenant->id, 'is_active' => true, 'daily_limit_target' => 5,
|
||
]);
|
||
|
||
$user = User::factory()->create(['tenant_id' => $tenant->id]);
|
||
|
||
// Сегодня — 3 заявки (received_at внутри текущего месяца — партиции deals помесячные).
|
||
Deal::factory()->count(3)->create([
|
||
'tenant_id' => $tenant->id,
|
||
'project_id' => $project->id,
|
||
'received_at' => now()->startOfDay()->addHours(9),
|
||
]);
|
||
|
||
$card = app(ClientFacts::class)->card((int) $user->id);
|
||
expect($card)->not->toBeNull();
|
||
|
||
// Проверяем саму ловушку ДО сторожа: если это условие не выполняется — подстроенные
|
||
// данные сломались, а не сторож (иначе тесты ниже могли бы пройти по случайной причине).
|
||
expect($card)->toContain('40 заявок')
|
||
->and($card)->toContain('получено в этом месяце 12')
|
||
->and($card)->toContain('Заявки: сегодня 3');
|
||
|
||
return (string) $card;
|
||
}
|
||
|
||
it('а) враньё режется: «получено 40» — число из ДРУГОГО факта настоящей карточки (баланс)', function () {
|
||
$card = guardContractRealCard();
|
||
$guard = app(AnswerGuard::class);
|
||
|
||
expect($guard->clean('В этом месяце получено 40 заявок.', $card))->toBe('');
|
||
});
|
||
|
||
it('б) враньё режется: «сегодня 12» — число из ДРУГОГО факта настоящей карточки (месяц)', function () {
|
||
$card = guardContractRealCard();
|
||
$guard = app(AnswerGuard::class);
|
||
|
||
expect($guard->clean('Сегодня пришло 12 заявок.', $card))->toBe('');
|
||
});
|
||
|
||
it('в) честное остаётся: «получено 12» — ровно то число, что в настоящей карточке', function () {
|
||
$card = guardContractRealCard();
|
||
$guard = app(AnswerGuard::class);
|
||
|
||
expect($guard->clean('В этом месяце получено 12 заявок.', $card))
|
||
->toBe('В этом месяце получено 12 заявок.');
|
||
});
|
||
|
||
it('г) честное остаётся: «сегодня 3» — ровно то число, что в настоящей карточке', function () {
|
||
$card = guardContractRealCard();
|
||
$guard = app(AnswerGuard::class);
|
||
|
||
expect($guard->clean('Сегодня пришло 3 заявки.', $card))
|
||
->toBe('Сегодня пришло 3 заявки.');
|
||
});
|