Files
autocrm/app/Http/Controllers/TelegramWebhookController.php
T
Vasyka 85ef2f6e00 Stage 13 — Notifications: Telegram bot + multi-channel + service reminders
Schema:
- clients.telegram_chat_id (linked via /start contact-share)
- clients.notify_prefs (per-client channel order override)
- service_reminders_sent (dedup ledger for the daily cron)

Telegram (per tenant):
- TelegramService (sendMessage, getMe, setWebhook with auto-generated secret)
- Bot token stored in companies.settings.telegram.bot_token
- Webhook /telegram/webhook/{slug} validates X-Telegram-Bot-Api-Secret-Token,
  matches client by last 9 digits of phone, persists chat_id, replies confirm
- /start prompts share-contact; /stop unlinks chat_id

NotificationDispatcher refactor:
- Multi-channel: telegram first if chat_id + bot configured, then email
- Backwards-compat with legacy boolean notify.{type} flags
- 4 HTML-formatted Telegram messages (wo_ready with tracking link, payment,
  appointment, reminder)

Service reminders:
- `reminders:send` artisan command with --slug / --dry-run
- Policy: vehicles whose last closed WO is older than reminder.after_days
  (default 365). Skips if sent within reminder.cooldown_days (default 30).
- Schedule daily 09:00

Filament UI:
- Settings page: Telegram bot token field + "Test bot" + "Set webhook" actions
- Settings page: reminder_after_days + reminder_cooldown_days inputs
- ClientResource: telegram_chat_id readonly badge

Tests (6 new, all pass):
- webhook links client via shared contact
- webhook rejects wrong secret → 401
- dispatcher uses telegram when chat_id present (Http::fake)
- dispatcher falls back to email otherwise
- dispatcher returns false when no channel available
- reminder cron respects 30-day cooldown

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:14:17 +00:00

101 lines
3.6 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Central\Company;
use App\Models\Tenant\Client;
use App\Services\Notifications\TelegramService;
use App\Tenancy\TenantManager;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
/**
* Receives Telegram updates per tenant. URL: /telegram/webhook/{slug}
*
* To link a Telegram account to a Client record, the bot expects the user
* to share their phone via Telegram's contact share button (Telegram lets
* users send their own phone with one tap). We match the shared phone (or
* the message text fallback) to clients.phone and persist chat_id.
*/
class TelegramWebhookController extends Controller
{
public function handle(Request $request, string $slug, TelegramService $telegram)
{
$company = Company::where('slug', $slug)->first();
if (! $company) return response()->json(['ok' => false], 404);
$expectedSecret = $telegram->webhookSecretFor($company);
$providedSecret = $request->header('X-Telegram-Bot-Api-Secret-Token');
if ($expectedSecret && $providedSecret !== $expectedSecret) {
Log::warning('telegram.webhook bad secret', ['tenant' => $slug]);
return response()->json(['ok' => false], 401);
}
app(TenantManager::class)->setCurrent($company);
$message = $request->input('message', []);
$chatId = (string) data_get($message, 'chat.id', '');
if (! $chatId) {
return response()->json(['ok' => true]);
}
$contact = data_get($message, 'contact');
$text = trim((string) data_get($message, 'text', ''));
$client = null;
$phoneRaw = null;
if ($contact) {
$phoneRaw = data_get($contact, 'phone_number');
} elseif (preg_match('/(\+?[0-9\-\s\(\)]{7,})/', $text, $m)) {
$phoneRaw = $m[1];
}
if ($phoneRaw) {
$needle = Client::normalizePhone($phoneRaw);
if ($needle) {
$client = Client::whereRaw(
"REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', '') LIKE ?",
['%' . substr($needle, -9) . '%']
)->first();
}
}
if (! $client && $text === '/start') {
$telegram->sendMessage($company, $chatId,
'Salut! Pentru a primi notificări despre mașina ta, ' .
'apasă butonul „Share contact" sau trimite numărul tău de telefon.'
);
return response()->json(['ok' => true]);
}
if ($client) {
$client->telegram_chat_id = $chatId;
$client->saveQuietly();
$name = $company->display_name ?? $company->name;
$telegram->sendMessage($company, $chatId,
"Te-am identificat — <b>{$client->name}</b>.\n" .
"Vei primi aici notificări despre fișele tale de la <b>{$name}</b>.\n\n" .
"Trimite /stop oricând ca să oprești notificările."
);
return response()->json(['ok' => true]);
}
if ($text === '/stop') {
Client::where('telegram_chat_id', $chatId)->update(['telegram_chat_id' => null]);
$telegram->sendMessage($company, $chatId, 'Notificările au fost oprite.');
return response()->json(['ok' => true]);
}
if ($phoneRaw) {
$telegram->sendMessage($company, $chatId,
"Nu am găsit un client cu acest număr la {$company->name}. " .
"Verifică telefonul sau contactează service-ul."
);
}
return response()->json(['ok' => true]);
}
}