diff --git a/app/app/Services/Project/ProjectRuleMessages.php b/app/app/Services/Project/ProjectRuleMessages.php new file mode 100644 index 00000000..93c733fb --- /dev/null +++ b/app/app/Services/Project/ProjectRuleMessages.php @@ -0,0 +1,92 @@ + 'января', 2 => 'февраля', 3 => 'марта', 4 => 'апреля', + 5 => 'мая', 6 => 'июня', 7 => 'июля', 8 => 'августа', + 9 => 'сентября', 10 => 'октября', 11 => 'ноября', 12 => 'декабря', + ]; + + public function formatDate(CarbonInterface $date): string + { + $msk = $date->copy()->setTimezone('Europe/Moscow'); + + return $msk->day.' '.self::MONTHS_GENITIVE[$msk->month]; + } + + /** Проект создан → слепок: эффект со следующего дня. */ + public function projectCreated(CarbonInterface $firstLeadDate): string + { + return 'Проект в сборе. Первые лиды пойдут с '.$this->formatDate($firstLeadDate).'.'; + } + + /** Изменён лимит/регион/дни после порога → правка вступит в силу позже. */ + public function changesApplyFrom(CarbonInterface $appliesFrom): string + { + return 'Изменения вступят в силу с '.$this->formatDate($appliesFrom).'.'; + } + + /** Смена источника подтверждена → хвост старого источника. */ + public function sourceChanged(CarbonInterface $tailUntil): string + { + return 'Лиды по старому источнику придут до '.$this->formatDate($tailUntil) + .', дальше — по новому.'; + } + + /** Проект на паузе → grace-хвост после паузы. */ + public function projectPaused(CarbonInterface $graceUntil): string + { + return 'Сбор остановится сегодня в 18:00. До '.$this->formatDate($graceUntil) + .' ещё могут прийти уже заказанные лиды — это нормально.'; + } + + /** Проект возобновлён → слепок на следующий день. */ + public function projectResumed(CarbonInterface $firstLeadDate): string + { + return 'Проект снова в сборе. Лиды пойдут с '.$this->formatDate($firstLeadDate).'.'; + } + + /** Баланс заканчивается → preflight/заморозка. */ + public function balanceRunningOut(int $leads): string + { + return 'Баланса хватит примерно на '.$leads.' '.$this->leadsWord($leads) + .'. Пополните, чтобы сбор не остановился.'; + } + + /** Склонение слова «лид» по числу. */ + private function leadsWord(int $n): string + { + $mod100 = $n % 100; + $mod10 = $n % 10; + if ($mod100 >= 11 && $mod100 <= 14) { + return 'лидов'; + } + if ($mod10 === 1) { + return 'лид'; + } + if ($mod10 >= 2 && $mod10 <= 4) { + return 'лида'; + } + + return 'лидов'; + } +} diff --git a/app/tests/Unit/Services/Project/ProjectRuleMessagesTest.php b/app/tests/Unit/Services/Project/ProjectRuleMessagesTest.php new file mode 100644 index 00000000..7e8c949f --- /dev/null +++ b/app/tests/Unit/Services/Project/ProjectRuleMessagesTest.php @@ -0,0 +1,51 @@ +changesApplyFrom(CarbonImmutable::parse('2026-06-27 21:00', 'Europe/Moscow')); + expect($msg)->toContain('27 июня'); +}); + +it('формат даты — род. падеж «D MMMM» по МСК', function (): void { + expect(ruleMessages()->formatDate(CarbonImmutable::parse('2026-01-01 12:00', 'Europe/Moscow')))->toBe('1 января'); + expect(ruleMessages()->formatDate(CarbonImmutable::parse('2026-05-09 00:00', 'Europe/Moscow')))->toBe('9 мая'); + expect(ruleMessages()->formatDate(CarbonImmutable::parse('2026-12-31 23:00', 'Europe/Moscow')))->toBe('31 декабря'); +}); + +it('текст смены источника объясняет хвост старого источника', function (): void { + $msg = ruleMessages()->sourceChanged(CarbonImmutable::parse('2026-06-27 21:00', 'Europe/Moscow')); + expect($msg)->toContain('по старому источнику') + ->and($msg)->toContain('27 июня') + ->and($msg)->toContain('дальше — по новому'); +}); + +it('текст паузы объясняет grace-хвост как норму', function (): void { + $msg = ruleMessages()->projectPaused(CarbonImmutable::parse('2026-06-27 21:00', 'Europe/Moscow')); + expect($msg)->toContain('18:00') + ->and($msg)->toContain('это нормально'); +}); + +it('текст создания/возобновления — когда пойдут первые лиды', function (): void { + $d = CarbonImmutable::parse('2026-06-26 21:00', 'Europe/Moscow'); + expect(ruleMessages()->projectCreated($d))->toContain('Первые лиды пойдут с 26 июня'); + expect(ruleMessages()->projectResumed($d))->toContain('Лиды пойдут с 26 июня'); +}); + +it('текст баланса склоняет слово «лид» по числу', function (): void { + expect(ruleMessages()->balanceRunningOut(21))->toContain('21 лид.'); + expect(ruleMessages()->balanceRunningOut(3))->toContain('3 лида'); + expect(ruleMessages()->balanceRunningOut(11))->toContain('11 лидов'); + expect(ruleMessages()->balanceRunningOut(105))->toContain('105 лидов'); +});