From be082396341aad5c7f8fa216bacb3660f3c49864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 25 Jun 2026 18:56:47 +0300 Subject: [PATCH] =?UTF-8?q?feat/projects:=20=D0=B5=D0=B4=D0=B8=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=20?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=BE=D0=B2=20=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=20=D1=81=D0=B1=D0=BE=D1=80=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=B0=20(?= =?UTF-8?q?=D0=AD=D0=BF=D0=B8=D0=BA=206.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProjectRuleMessages — 6 методов (создан/изменён/смена источника/пауза/возобновление/баланс) с русским форматом даты «D MMMM» и склонением «лид». Единственный источник текстов: in-app уведомления (6.2) и баннеры (6.3) тянут отсюда, не дублируют строки. 6/6 тестов. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Services/Project/ProjectRuleMessages.php | 92 +++++++++++++++++++ .../Project/ProjectRuleMessagesTest.php | 51 ++++++++++ 2 files changed, 143 insertions(+) create mode 100644 app/app/Services/Project/ProjectRuleMessages.php create mode 100644 app/tests/Unit/Services/Project/ProjectRuleMessagesTest.php 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 лидов'); +});