Живая ошибка на боевом. Пополнение клиента в 01:00 попадало во вчерашний день,
а первые три часа первого числа месяца — в прошлый месяц. Руководитель видел
деньги не за тот день, и ни ошибки, ни следа в журнале при этом не было.
Причина. Границу суток портал считает по Москве, а в базу отправляет БЕЗ пояса —
просто «02.08 00:00». Сеанс базы живёт по Гринвичу и читает эту надпись как
гринвичскую, то есть как 03:00 по Москве. Замер прямым запросом: граница
«полночь по Москве» превращается в базе в «три часа ночи по Москве».
Лечение. Границы периода уходят в запрос МГНОВЕНИЯМИ, а не надписью на часах.
Правило названо в одном месте — в описании периода (SalesPeriodRange) — двумя
методами startInstant / endExclusiveInstant, и там же сказано, где их брать НЕЛЬЗЯ:
для сравнения с календарной датой (paid_on) и для счёта дней нужен именно
московский календарь, там перевод в UTC всё сломает.
Почему это не ловилось раньше. Ошибка видна ТОЛЬКО ночью: тот же код в 23:55
давал зелёный прогон, в 00:20 — красный. Попалась случайно, прогон пересёк
полночь. Оба теста теперь морозят часы на 00:30 МСК — то есть проверяют дыру
в любое время суток, а не когда повезёт. Часы возвращаются в afterEach.
Отдельная находка того же захода: ЧЕТЫРЕ проверки границ ЗАКРЕПЛЯЛИ ошибку.
Они клали данные голой строкой (то есть по Гринвичу), а период строили по Москве —
две ошибки гасили друг друга. Починил границу — они честно покраснели. Данные
в них теперь тоже московские, и пояс пишется прямо в строке: объект даты по
дороге в базу пояс теряет, а строка доходит как есть.
Приёмка вырезанием в обе стороны: убрал перевод в мгновения — покраснели три
проверки, включая обе денежные; вернул — папка продаж 481 из 481 зелёная.
Полный прогон: 4111 тестов, 4107 зелёных, 4 пропущено, 0 падений. Статанализ 0.
После полного прогона база чиста — сторож чистоты подтверждает.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
balance_transactions партиции создаются динамически (partitions:create-months = текущий+будущие), НЕ в schema.sql → при migrate:fresh общей тест-БД прошлые месяцы не восстанавливаются, июньские топап-тесты падали 'нет секции'. Привязал 4 balance_transactions-теста к ТЕКУЩЕМУ месяцу (btDate/btThisMonthRange/btLastDay/btNextMonthStart) — у него партиция есть всегда. deals/lead_charges тесты не трогал (их партиции в схеме, переживают fresh). 15/15. Один эскейп на сессию.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 1.2: SalesMetricsService — leadsDelivered/oborotRub/topupsRub/cumulativeTopupsRub/runwayDays. Деньги: оборот из целых копеек (/100 в конце), полуоткрытые интервалы [start, след.день). runwayDays реиспользует PricingTierRepository→BalanceToLeadsConverter→RunwayCalculator (совпадает с кабинетом клиента, F3). leadsDelivered 1:1 с DashboardController (deleted_at NULL, is_test=false). Тест 15/15 (с граничными), stan 0. Один эскейп на сессию.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>