Files
portal/app/tests/Unit/Sales/SalesPeriodResolverTest.php
T
Дмитрий 947cb3403a
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
fix(воронка-продаж): сроки фильтра по датам — «Просроченные» отдельным пунктом, планы смотрят вперёд
Просьба владельца 01.08.2026: «убери вчера, наверх создай просроченные,
сегодня, завтра и т.д. и проверь что они правильно привязаны и реально
работают»; во втором режиме «убери завтра — завтра у тебя не может быть».

У каждого режима теперь СВОЙ список сроков, они не пересекаются:

  Что надо сделать (вперёд): Просроченные / Сегодня / Завтра /
                             Ближайшие 7 дней / Ближайшие 30 дней / Произвольный
  Что менялось (назад):      Сегодня / Вчера / 7 дней / 30 дней / Произвольный

Каждый пункт показывает ровно то, что на нём написано. Раньше просроченное
подмешивалось в ЛЮБОЙ выбранный период, и «Сегодня» показывало не только
сегодняшнее — теперь это отдельный первый пункт (period=overdue), а подпись
«плюс всё просроченное» убрана за ненадобностью.

Вторая, невидимая глазом поломка: «7/30 дней» в режиме планов считались
НАЗАД (d7/d30) — «что надо сделать за прошедшую неделю». Добавлены зеркала
next7/next30 в SalesPeriodResolver.

Срок из чужого набора («что менялось завтра») сервер отвергает с 422, а не
подменяет молча текущим месяцем, как делал прежний резолвер по умолчанию.

Проверено:
- сервер: 30/30 (фильтр + резолвер), весь отдел продаж 498/498;
- фронт: 1739/1739 весь набор;
- приёмка вырезанием — подложил поломку в оба места, оба набора покраснели;
- глазами в браузере 1920×1080: списки сроков в обоих режимах, «Просроченные»
  дают ровно забытые карточки, «Завтра» — ровно завтрашнюю (скрины 08–12).

Попутно починен чужой протухший тест advertising-channels: Телеграм давно
стал живым роутом, а тест продолжал считать его заглушкой и был красным.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 975175750a)
2026-08-01 16:16:51 +03:00

177 lines
7.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Services\Sales\SalesPeriodRange;
use App\Services\Sales\SalesPeriodResolver;
use Carbon\CarbonImmutable;
// Заморозка времени: 15 июня 2026, 12:00 МСК
beforeEach(function (): void {
CarbonImmutable::setTestNow(
CarbonImmutable::parse('2026-06-15 12:00:00', 'Europe/Moscow'),
);
});
afterEach(function (): void {
CarbonImmutable::setTestNow();
});
// ─── kind=this ────────────────────────────────────────────────────────────────
it('kind=this returns current month range (June 2026)', function (): void {
$resolver = new SalesPeriodResolver;
$range = $resolver->resolve(['kind' => 'this']);
expect($range)->toBeInstanceOf(SalesPeriodRange::class);
expect($range->start->format('Y-m-d H:i:s'))->toBe('2026-06-01 00:00:00');
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-06-30 23:59:59');
});
// ─── kind=prev ────────────────────────────────────────────────────────────────
it('kind=prev returns previous month range (May 2026)', function (): void {
$resolver = new SalesPeriodResolver;
$range = $resolver->resolve(['kind' => 'prev']);
expect($range->start->format('Y-m-d H:i:s'))->toBe('2026-05-01 00:00:00');
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-05-31 23:59:59');
});
// ─── kind=prev2 ───────────────────────────────────────────────────────────────
it('kind=prev2 returns month-before-previous range (April 2026)', function (): void {
$resolver = new SalesPeriodResolver;
$range = $resolver->resolve(['kind' => 'prev2']);
expect($range->start->format('Y-m-d H:i:s'))->toBe('2026-04-01 00:00:00');
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-04-30 23:59:59');
});
// ─── kind=custom ──────────────────────────────────────────────────────────────
it('kind=custom returns exact from/to bounds', function (): void {
$resolver = new SalesPeriodResolver;
$range = $resolver->resolve([
'kind' => 'custom',
'from' => '2026-03-10',
'to' => '2026-05-20',
]);
expect($range->start->format('Y-m-d H:i:s'))->toBe('2026-03-10 00:00:00');
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-05-20 23:59:59');
});
it('kind=custom: monthsIn returns all three month-starts for Mar-May span', function (): void {
$resolver = new SalesPeriodResolver;
$range = $resolver->resolve([
'kind' => 'custom',
'from' => '2026-03-10',
'to' => '2026-05-20',
]);
$months = $resolver->monthsIn($range);
expect($months)->toHaveCount(3);
expect($months[0]->format('Y-m-d'))->toBe('2026-03-01');
expect($months[1]->format('Y-m-d'))->toBe('2026-04-01');
expect($months[2]->format('Y-m-d'))->toBe('2026-05-01');
});
it('kind=custom: throws InvalidArgumentException when from > to', function (): void {
$resolver = new SalesPeriodResolver;
expect(fn () => $resolver->resolve([
'kind' => 'custom',
'from' => '2026-05-20',
'to' => '2026-03-10',
]))->toThrow(InvalidArgumentException::class);
});
it('kind=custom: throws InvalidArgumentException when from is missing', function (): void {
$resolver = new SalesPeriodResolver;
expect(fn () => $resolver->resolve([
'kind' => 'custom',
'to' => '2026-05-20',
]))->toThrow(InvalidArgumentException::class);
});
it('kind=custom: throws InvalidArgumentException when to is missing', function (): void {
$resolver = new SalesPeriodResolver;
expect(fn () => $resolver->resolve([
'kind' => 'custom',
'from' => '2026-03-10',
]))->toThrow(InvalidArgumentException::class);
});
// ─── monthsIn ─────────────────────────────────────────────────────────────────
it('monthsIn for single-month "this" range returns one entry (June 2026)', function (): void {
$resolver = new SalesPeriodResolver;
$range = $resolver->resolve(['kind' => 'this']);
$months = $resolver->monthsIn($range);
expect($months)->toHaveCount(1);
expect($months[0]->format('Y-m-d'))->toBe('2026-06-01');
});
// ─── timezone ─────────────────────────────────────────────────────────────────
it('range start carries Europe/Moscow timezone', function (): void {
$resolver = new SalesPeriodResolver;
$range = $resolver->resolve(['kind' => 'this']);
expect($range->start->timezoneName)->toBe('Europe/Moscow');
});
// ─── новые периоды: сегодня / вчера / 7 дней / 30 дней ────────────────────────
it('kind=today — только сегодняшний день', function (): void {
$range = (new SalesPeriodResolver)->resolve(['kind' => 'today']);
expect($range->start->format('Y-m-d H:i:s'))->toBe('2026-06-15 00:00:00');
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-06-15 23:59:59');
});
it('kind=yesterday — только вчерашний день', function (): void {
$range = (new SalesPeriodResolver)->resolve(['kind' => 'yesterday']);
expect($range->start->format('Y-m-d H:i:s'))->toBe('2026-06-14 00:00:00');
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-06-14 23:59:59');
});
it('kind=tomorrow — только завтрашний день', function (): void {
$range = (new SalesPeriodResolver)->resolve(['kind' => 'tomorrow']);
expect($range->start->format('Y-m-d H:i:s'))->toBe('2026-06-16 00:00:00');
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-06-16 23:59:59');
});
it('kind=next7 — ближайшие 7 дней ВПЕРЁД, включая сегодня', function (): void {
$range = (new SalesPeriodResolver)->resolve(['kind' => 'next7']);
expect($range->start->format('Y-m-d H:i:s'))->toBe('2026-06-15 00:00:00');
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-06-21 23:59:59');
});
it('kind=next30 — ближайшие 30 дней ВПЕРЁД, включая сегодня', function (): void {
$range = (new SalesPeriodResolver)->resolve(['kind' => 'next30']);
expect($range->start->format('Y-m-d H:i:s'))->toBe('2026-06-15 00:00:00');
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-07-14 23:59:59');
});
it('kind=d7 — последние 7 дней, включая сегодня', function (): void {
$range = (new SalesPeriodResolver)->resolve(['kind' => 'd7']);
expect($range->start->format('Y-m-d H:i:s'))->toBe('2026-06-09 00:00:00');
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-06-15 23:59:59');
});
it('kind=d30 — последние 30 дней, включая сегодня', function (): void {
$range = (new SalesPeriodResolver)->resolve(['kind' => 'd30']);
expect($range->start->format('Y-m-d H:i:s'))->toBe('2026-05-17 00:00:00');
expect($range->end->format('Y-m-d H:i:s'))->toBe('2026-06-15 23:59:59');
});