85ef2f6e00
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>
101 lines
3.6 KiB
PHP
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]);
|
|
}
|
|
}
|