Files
portal/app/app/Services/Bot/LivePrices.php
T
Дмитрий 47afefbb10 feat(bot): цены бот тянет сам из настроек — вручную актуализировать больше не надо
Вопрос владельца: «а цены в нём надо актуализировать руками или он сам тянет?»
Ответ был неприятный: руками. Цифры (300/50 руб, тарифные ступени) стояли текстом
в статьях — поменяли бы цену в админке, бот продолжил бы врать про деньги.

Теперь в статьях стоят метки {{цена_подбора}}, {{цена_изучения}}, {{тарифы}},
а сервис LivePrices подставляет живые значения из system_settings и pricing_tiers
В МОМЕНТ ОТВЕТА — без переиндексации базы знаний. Нет цены в настройках — метка
не протекает клиенту, бот отсылает к экрану, где цена написана всегда.

Из текстов экскурсий цифры цен тоже убраны (фронт не знает БД): шаг подсвечивает
строку с ценой в самом окне — она всегда актуальная.

Проверено живьём: поменял цену в настройках на 450 — бот сразу назвал 450.
Pest 42/42 (3 новых теста), Vitest каталога 8/8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:06:04 +03:00

71 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Bot;
use App\Support\SystemSettings;
use Illuminate\Support\Facades\DB;
/**
* Живые цены для ответов бота (вопрос владельца 12.07.2026: «цены надо
* актуализировать руками или он сам тянет?»).
*
* В статьях инструкции цены стоят МЕТКАМИ, а не цифрами: {{цена_подбора}},
* {{цена_изучения}}, {{тарифы}}. Подстановка происходит в момент ответа —
* поменяли цену в админке, бот называет новую сразу, без переиндексации.
*
* Если цены в настройках нет — метку не показываем клиенту, а мягко отсылаем
* к экрану, где цена написана всегда.
*/
class LivePrices
{
private const FALLBACK_PRICE = 'по цене, указанной в самом окне';
public function substitute(string $text): string
{
if (! str_contains($text, '{{')) {
return $text;
}
return strtr($text, [
'{{цена_подбора}}' => $this->rub(SystemSettings::get('autopodbor_price_search_rub')),
'{{цена_изучения}}' => $this->rub(SystemSettings::get('autopodbor_price_study_rub')),
'{{тарифы}}' => $this->tiers(),
]);
}
private function rub(?string $value): string
{
if ($value === null || $value === '' || ! is_numeric($value)) {
return self::FALLBACK_PRICE;
}
return rtrim(rtrim(number_format((float) $value, 2, ',', ' '), '0'), ',').' ₽';
}
/** Сетка ступеней словами: «1-я — 500 ₽ за заявку (первые 100 заявок месяца)». */
private function tiers(): string
{
$rows = DB::table('pricing_tiers')
->where('is_active', true)
->orderBy('tier_no')
->get(['tier_no', 'leads_in_tier', 'price_per_lead_kopecks']);
if ($rows->isEmpty()) {
return 'сетка ступеней показана в разделе «Биллинг»';
}
$parts = [];
foreach ($rows as $r) {
$price = $this->rub((string) ((int) $r->price_per_lead_kopecks / 100));
$count = (int) $r->leads_in_tier;
$parts[] = $count > 0
? "ступень {$r->tier_no}{$price} за заявку (следующие {$count} заявок месяца)"
: "ступень {$r->tier_no}{$price} за заявку (весь объём сверх предыдущих)";
}
return implode('; ', $parts);
}
}