companyFor($wo); $client = $wo->client; if (! $client) return false; return $this->dispatch($company, $client, 'wo_ready', [ 'telegram' => fn () => $this->tgWorkOrderReady($wo, $company, $client), 'email' => fn () => $this->emailSafe( fn () => Mail::to($client->email)->send(new WorkOrderReadyMail($wo, $company)), 'workOrderReady', ['wo' => $wo->id] ), ]); } public function paymentReceived(Payment $payment): bool { $company = $this->companyFor($payment); $client = $payment->client; if (! $client) return false; return $this->dispatch($company, $client, 'payment', [ 'telegram' => fn () => $this->tgPaymentReceived($payment, $company, $client), 'email' => fn () => $this->emailSafe( fn () => Mail::to($client->email)->send(new PaymentReceivedMail($payment, $company)), 'paymentReceived', ['payment' => $payment->id] ), ]); } public function appointmentConfirmed(Appointment $a): bool { $company = $this->companyFor($a); $client = $a->client; if (! $client) return false; return $this->dispatch($company, $client, 'appointment', [ 'telegram' => fn () => $this->tgAppointmentConfirmed($a, $company, $client), 'email' => fn () => $this->emailSafe( fn () => Mail::to($client->email)->send(new AppointmentConfirmedMail($a, $company)), 'appointmentConfirmed', ['appt' => $a->id] ), ]); } public function serviceReminder(Vehicle $v, string $type = 'general', ?string $note = null): bool { $company = $this->companyFor($v); $client = $v->client; if (! $client) return false; return $this->dispatch($company, $client, 'reminder', [ 'telegram' => fn () => $this->tgServiceReminder($v, $type, $note, $company, $client), 'email' => fn () => $this->emailSafe( fn () => Mail::to($client->email)->send(new ServiceReminderMail($v, $type, $note, $company)), 'serviceReminder', ['vehicle' => $v->id] ), ]); } public function tireSeasonalSwap(\App\Models\Tenant\TireSet $set): bool { $company = $this->companyFor($set); $client = $set->client; if (! $client) return false; return $this->dispatch($company, $client, 'reminder', [ 'telegram' => fn () => $this->tgTireSeasonalSwap($set, $company, $client), 'email' => fn () => $set->vehicle ? $this->emailSafe( fn () => Mail::to($client->email)->send(new ServiceReminderMail( $set->vehicle, 'tire_swap', 'E timpul să schimbi anvelopele ' . ($set->season === 'winter' ? 'de iarnă' : 'de vară') . ' (' . $set->sizeLabel() . ').', $company )), 'tireSeasonalSwap', ['set' => $set->id] ) : false, ]); } protected function tgTireSeasonalSwap(\App\Models\Tenant\TireSet $set, Company $company, Client $client): bool { $brand = htmlspecialchars($company->display_name ?? $company->name); $size = htmlspecialchars($set->sizeLabel()); $seasonRo = $set->season === 'winter' ? 'de iarnă' : 'de vară'; $loc = $set->currentStorage()?->location; $plate = $set->vehicle?->plate ? ' · ' . htmlspecialchars($set->vehicle->plate) : ''; $text = "🔧 Schimb sezonier anvelope\n" . "Setul tău {$seasonRo} ({$size}){$plate}" . ($loc ? " e în depozit la {$loc}." : '.') . "\n\nProgramează-te la {$brand}."; return $this->telegram->sendMessage($company, (string) $client->telegram_chat_id, $text); } // ─── Channel dispatch ───────────────────────────────────────── /** * @param array $senders channel-key → sender callback * @return bool Returns the channel name that delivered, or null on full miss. */ protected function dispatch(Company $company, Client $client, string $key, array $senders): bool { $any = false; foreach ($this->channelsFor($company, $client, $key) as $channel) { if (! isset($senders[$channel])) continue; try { if (($senders[$channel])() === true) { $any = true; // Try only one channel — first that succeeds is enough. break; } } catch (\Throwable $e) { Log::warning("notify.{$key} {$channel} threw", ['err' => $e->getMessage()]); } } return $any; } /** * Resolve which channels to try and in what order, applying per-client * preference if set, otherwise the tenant default. */ protected function channelsFor(Company $company, Client $client, string $key): array { $prefs = (array) ($client->notify_prefs ?? []); // Per-client list takes precedence. if (! empty($prefs)) { $candidates = array_values(array_intersect(['telegram', 'email'], $prefs)); } else { $candidates = ['telegram', 'email']; } return array_values(array_filter( $candidates, fn (string $ch) => $this->channelEnabled($company, $client, $key, $ch) )); } protected function channelEnabled(Company $company, Client $client, string $key, string $channel): bool { $notify = (array) data_get($company->settings, 'notify', []); // Tenant-level: notify.{type}.{channel} — accept legacy boolean notify.{type}=true as email-only flag. $tenantValue = $notify[$key] ?? null; if (is_bool($tenantValue)) { // legacy: email default true, telegram default true if bot configured if ($channel === 'email') $tenantValueEnabled = $tenantValue; elseif ($channel === 'telegram') $tenantValueEnabled = $tenantValue && (bool) $this->telegram->tokenFor($company); else $tenantValueEnabled = false; } elseif (is_array($tenantValue)) { $tenantValueEnabled = (bool) ($tenantValue[$channel] ?? true); } else { $tenantValueEnabled = true; } if (! $tenantValueEnabled) return false; // Per-client field presence if ($channel === 'telegram') { return ! empty($client->telegram_chat_id) && (bool) $this->telegram->tokenFor($company); } if ($channel === 'email') { return ! empty($client->email); } return false; } // ─── Telegram message builders ──────────────────────────────── protected function tgWorkOrderReady(WorkOrder $wo, Company $company, Client $client): bool { $brand = htmlspecialchars($company->display_name ?? $company->name); $no = htmlspecialchars((string) $wo->number); $plate = htmlspecialchars((string) ($wo->vehicle->plate ?? '')); $text = "✅ Mașina e gata de ridicat\n" . "Fișa #{$no} · {$plate}\n" . "Total: " . number_format((float) $wo->total, 2) . " " . ($company->settings['currency'] ?? 'MDL') . "\n\n" . "🔗 Detalii: " . $wo->trackingUrl() . "\n\n" . "{$brand}"; return $this->telegram->sendMessage($company, (string) $client->telegram_chat_id, $text); } protected function tgPaymentReceived(Payment $payment, Company $company, Client $client): bool { $amt = number_format((float) $payment->amount, 2); $cur = $company->settings['currency'] ?? 'MDL'; $text = "💳 Plată primită\n" . "Suma: {$amt} {$cur}\n" . "Mulțumim!"; return $this->telegram->sendMessage($company, (string) $client->telegram_chat_id, $text); } protected function tgAppointmentConfirmed(Appointment $a, Company $company, Client $client): bool { $when = $a->starts_at?->isoFormat('D MMM YYYY, HH:mm') ?? '?'; $text = "📅 Programare confirmată\n" . "Data: {$when}\n" . ($a->vehicle?->plate ? "Auto: " . htmlspecialchars($a->vehicle->plate) . "\n" : '') . "Te așteptăm."; return $this->telegram->sendMessage($company, (string) $client->telegram_chat_id, $text); } protected function tgServiceReminder(Vehicle $v, string $type, ?string $note, Company $company, Client $client): bool { $brand = htmlspecialchars($company->display_name ?? $company->name); $plate = htmlspecialchars((string) ($v->plate ?? '')); $text = "🔧 Reminder service\n" . "{$brand}: " . htmlspecialchars($v->make . ' ' . $v->model) . " · {$plate}\n" . ($note ?: 'A trecut ceva timp de la ultima vizită — recomandăm o verificare.'); return $this->telegram->sendMessage($company, (string) $client->telegram_chat_id, $text); } // ─── Helpers ────────────────────────────────────────────────── protected function emailSafe(callable $fn, string $tag, array $ctx = []): bool { try { $fn(); return true; } catch (\Throwable $e) { Log::warning("{$tag} mail failed", $ctx + ['err' => $e->getMessage()]); return false; } } protected function companyFor($model): Company { return Company::withoutGlobalScopes()->findOrFail($model->company_id); } }