orderStatus?->name ?? (Order::STATUS_NAMES[$order->order_status_id] ?? '-'); $this->notifyOrderEvent( $order, UserNotification::EVENT_CREATED, 'Площадки', sprintf('Добавлена новая площадка %s', $order->object_address), sprintf( 'Добавлена новая площадка %s.', route('order.show', ['order' => $order->id]), e($order->object_address) ), $statusName, ); } public function notifyOrderStatusChanged(Order $order): void { $statusName = $order->orderStatus?->name ?? (Order::STATUS_NAMES[$order->order_status_id] ?? '-'); $this->notifyOrderEvent( $order, UserNotification::EVENT_STATUS_CHANGED, 'Площадки', sprintf('Статус площадки %s изменен на %s', $order->object_address, $statusName), sprintf( 'Статус площадки %s изменен на %s.', route('order.show', ['order' => $order->id]), e($order->object_address), e($statusName) ), $statusName, ); } public function notifyReclamationCreated(Reclamation $reclamation): void { $order = $reclamation->order; if (!$order) { return; } $message = sprintf( 'Добавлена новая рекламация по адресу %s #%d', $order->object_address, $reclamation->id, ); $messageHtml = sprintf( 'Добавлена новая рекламация по адресу %s #%d.', route('order.show', ['order' => $order->id]), e($order->object_address), route('reclamations.show', ['reclamation' => $reclamation->id]), $reclamation->id, ); $this->notifyReclamationEvent( $reclamation, UserNotification::EVENT_CREATED, 'Рекламации', $message, $messageHtml, (int)$reclamation->status_id, ); } public function notifyReclamationStatusChanged(Reclamation $reclamation): void { $order = $reclamation->order; if (!$order) { return; } $statusName = $reclamation->status?->name ?? (Reclamation::STATUS_NAMES[$reclamation->status_id] ?? '-'); $message = sprintf( 'Статус рекламации %s #%d изменен на %s', $order->object_address, $reclamation->id, $statusName, ); $messageHtml = sprintf( 'Статус рекламации по адресу %s #%d изменен на %s.', route('order.show', ['order' => $order->id]), e($order->object_address), route('reclamations.show', ['reclamation' => $reclamation->id]), $reclamation->id, e($statusName), ); $this->notifyReclamationEvent( $reclamation, UserNotification::EVENT_STATUS_CHANGED, 'Рекламации', $message, $messageHtml, (int)$reclamation->status_id, ); } public function notifyScheduleAdded(Schedule $schedule): void { $sourceKey = $this->sourceToSettingKey((string)$schedule->source); if (!$sourceKey) { return; } $brigadierName = $schedule->brigadier?->name ?? '-'; $date = DateHelper::getHumanDate((string)$schedule->installation_date, true); if ((string)$schedule->source === 'Рекламации') { $reclamationId = $this->extractReclamationId((string)$schedule->address_code); $message = sprintf( 'Рекламация №%s по адресу %s добавлена в график на %s, Бригадир %s.', $reclamationId ?? '—', $schedule->object_address, $date, $brigadierName, ); $reclamationLink = $reclamationId ? route('reclamations.show', ['reclamation' => $reclamationId]) : route('schedule.index'); $orderLink = $schedule->order_id ? route('order.show', ['order' => $schedule->order_id]) : null; $addressHtml = $orderLink ? sprintf('%s', $orderLink, e($schedule->object_address)) : e($schedule->object_address); $messageHtml = sprintf( 'Рекламация №%s по адресу %s добавлена в график на %s, Бригадир %s.', $reclamationLink, $reclamationId ?? '—', $addressHtml, e($date), e($brigadierName), ); } else { $message = sprintf( '%s добавлено в график монтажей на %s, Бригадир %s.', $schedule->object_address, $date, $brigadierName, ); $orderLink = $schedule->order_id ? route('order.show', ['order' => $schedule->order_id]) : route('schedule.index'); $messageHtml = sprintf( '%s добавлено в график монтажей на %s, Бригадир %s.', $orderLink, e($schedule->object_address), e($date), e($brigadierName), ); } $users = $this->scheduleRecipients($schedule); foreach ($users as $user) { $settings = $this->settingsForUser($user->id); if (!$settings->isSectionEnabled('schedule_settings')) { continue; } $channels = $settings->getChannelsForKey('schedule_settings', $sourceKey); if (empty($channels)) { continue; } $notification = $this->createInAppNotification( $user, UserNotification::TYPE_SCHEDULE, UserNotification::EVENT_SCHEDULE_ADDED, 'График монтажей', $message, $messageHtml, [ 'schedule_id' => $schedule->id, 'source' => $schedule->source, ], ); $this->dispatchDeliveryJobs($notification, [ NotificationDeliveryLog::CHANNEL_BROWSER => !empty($channels['browser']), NotificationDeliveryLog::CHANNEL_PUSH => !empty($channels['push']), NotificationDeliveryLog::CHANNEL_EMAIL => !empty($channels['email']), ]); } } public function deliverChannel(int $userNotificationId, string $channel, int $attempt): void { $notification = UserNotification::query()->with('user')->find($userNotificationId); if (!$notification || !$notification->user) { return; } try { if ($channel === NotificationDeliveryLog::CHANNEL_BROWSER) { event(new SendPersistentNotificationEvent($notification->user_id, [ 'id' => $notification->id, 'type' => $notification->type, 'title' => $notification->title, 'message' => $notification->message, 'message_html' => $notification->message_html, 'created_at' => $notification->created_at?->toDateTimeString(), ])); $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_SENT, $attempt, null); return; } if ($channel === NotificationDeliveryLog::CHANNEL_PUSH) { if (!$notification->user->token_fcm) { $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_SKIPPED, $attempt, 'Отсутствует token_fcm'); return; } $notification->user->notify(new FireBaseNotification($notification->title, Str::limit(strip_tags($notification->message), 200))); $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_SENT, $attempt, null); return; } if ($channel === NotificationDeliveryLog::CHANNEL_EMAIL) { $email = $notification->user->notification_email; if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) { $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_SKIPPED, $attempt, 'Отсутствует валидный notification_email'); return; } Mail::html($notification->message_html, function ($message) use ($email, $notification) { $message->to($email) ->subject($notification->title); }); $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_SENT, $attempt, null); return; } $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_SKIPPED, $attempt, 'Неизвестный канал'); } catch (\Throwable $exception) { $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_FAILED, $attempt, $exception->getMessage()); throw $exception; } } public function markDeadLetter(int $userNotificationId, string $channel, int $attempt, ?string $error = null): void { $notification = UserNotification::query()->find($userNotificationId); if (!$notification) { return; } $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_DEAD_LETTER, $attempt, $error); } private function notifyOrderEvent( Order $order, string $event, string $title, string $message, string $messageHtml, string $statusName, ): void { $users = $this->orderRecipients($order); $statusId = (int) $order->order_status_id; foreach ($users as $user) { $settings = $this->settingsForUser($user->id); if (!$settings->isSectionEnabled('order_settings')) { continue; } $channels = $settings->getChannelsForKey('order_settings', $statusId); if (empty($channels)) { continue; } $notification = $this->createInAppNotification( $user, UserNotification::TYPE_PLATFORM, $event, $title, $message, $messageHtml, [ 'order_id' => $order->id, 'status' => $statusName, ], ); $this->dispatchDeliveryJobs($notification, [ NotificationDeliveryLog::CHANNEL_BROWSER => !empty($channels['browser']), NotificationDeliveryLog::CHANNEL_PUSH => !empty($channels['push']), NotificationDeliveryLog::CHANNEL_EMAIL => !empty($channels['email']), ]); } } private function notifyReclamationEvent( Reclamation $reclamation, string $event, string $title, string $message, string $messageHtml, int $statusId, ): void { $users = $this->reclamationRecipients($reclamation); foreach ($users as $user) { $settings = $this->settingsForUser($user->id); if (!$settings->isSectionEnabled('reclamation_settings')) { continue; } $channels = $settings->getChannelsForKey('reclamation_settings', $statusId); if (empty($channels)) { continue; } $notification = $this->createInAppNotification( $user, UserNotification::TYPE_RECLAMATION, $event, $title, $message, $messageHtml, [ 'reclamation_id' => $reclamation->id, 'status_id' => $statusId, ], ); $this->dispatchDeliveryJobs($notification, [ NotificationDeliveryLog::CHANNEL_BROWSER => !empty($channels['browser']), NotificationDeliveryLog::CHANNEL_PUSH => !empty($channels['push']), NotificationDeliveryLog::CHANNEL_EMAIL => !empty($channels['email']), ]); } } private function dispatchDeliveryJobs(UserNotification $notification, array $channels): void { foreach ($channels as $channel => $enabled) { if (!$enabled) { continue; } SendUserNotificationChannelJob::dispatch($notification->id, $channel); } } private function createInAppNotification( User $user, string $type, string $event, string $title, string $message, string $messageHtml, array $payload, ): UserNotification { $notification = UserNotification::query()->create([ 'user_id' => $user->id, 'type' => $type, 'event' => $event, 'title' => $title, 'message' => $message, 'message_html' => $messageHtml, 'data' => $payload, ]); $this->createLog($notification, NotificationDeliveryLog::CHANNEL_IN_APP, NotificationDeliveryLog::STATUS_SENT, 1, null); return $notification; } private function createLog( UserNotification $notification, string $channel, string $status, int $attempt, ?string $error, ): void { NotificationDeliveryLog::query()->create([ 'user_notification_id' => $notification->id, 'user_id' => $notification->user_id, 'channel' => $channel, 'status' => $status, 'attempt' => $attempt, 'message' => $notification->message, 'error' => $error, ]); } private function settingsForUser(int $userId): UserNotificationSetting { return UserNotificationSetting::query()->firstOrCreate( ['user_id' => $userId], UserNotificationSetting::defaultsForUser($userId), ); } private function orderRecipients(Order $order): Collection { $query = User::query() ->whereIn('role', [Role::ADMIN, Role::ASSISTANT_HEAD, Role::WAREHOUSE_HEAD]); if ($order->user_id) { $query->orWhere('id', $order->user_id); } return $query->distinct()->get(); } private function reclamationRecipients(Reclamation $reclamation): Collection { $query = User::query() ->whereIn('role', [Role::ADMIN, Role::ASSISTANT_HEAD, Role::WAREHOUSE_HEAD]); $managerId = $reclamation->user_id; if ($managerId) { $query->orWhere('id', $managerId); } return $query->distinct()->get(); } private function scheduleRecipients(Schedule $schedule): Collection { $query = User::query() ->whereIn('role', [Role::ADMIN, Role::ASSISTANT_HEAD, Role::WAREHOUSE_HEAD]); if ($schedule->brigadier_id) { $query->orWhere('id', $schedule->brigadier_id); } $managerId = null; if ((string)$schedule->source === 'Площадки' && $schedule->order_id) { $managerId = Order::query() ->withoutGlobalScope(\App\Models\Scopes\YearScope::class) ->where('id', $schedule->order_id) ->value('user_id'); } if ((string)$schedule->source === 'Рекламации') { $reclamationId = $this->extractReclamationId((string)$schedule->address_code); if ($reclamationId) { $reclamation = Reclamation::query() ->withoutGlobalScope(\App\Models\Scopes\YearScope::class) ->with('order') ->find($reclamationId); $managerId = $reclamation?->order?->user_id ?: $reclamation?->user_id; } } if ($managerId) { $query->orWhere('id', $managerId); } return $query->distinct()->get(); } private function extractReclamationId(string $addressCode): ?int { if (preg_match('/^РЕКЛ-(\d+)$/u', $addressCode, $matches)) { return (int)$matches[1]; } return null; } private function sourceToSettingKey(string $source): ?string { return match ($source) { 'Площадки' => 'platform', 'Рекламации' => 'reclamation', default => null, }; } }