NotificationService.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. <?php
  2. namespace App\Services;
  3. use App\Enums\ProductionOrderNotificationEvent;
  4. use App\Events\SendPersistentNotificationEvent;
  5. use App\Helpers\DateHelper;
  6. use App\Jobs\SendUserNotificationChannelJob;
  7. use App\Models\ChatMessage;
  8. use App\Models\NotificationDeliveryLog;
  9. use App\Models\Order;
  10. use App\Models\ProductionOrder;
  11. use App\Models\ProductionOrderDelivery;
  12. use App\Models\ProductionOrderInstallation;
  13. use App\Models\Reclamation;
  14. use App\Models\Schedule;
  15. use App\Models\User;
  16. use App\Models\UserNotification;
  17. use App\Models\UserNotificationSetting;
  18. use App\Notifications\FireBaseNotification;
  19. use Illuminate\Database\Eloquent\Collection;
  20. use Illuminate\Support\Facades\Mail;
  21. use Illuminate\Support\Str;
  22. class NotificationService
  23. {
  24. public function notifyProductionOrderEvent(
  25. ProductionOrder $order,
  26. ProductionOrderNotificationEvent $event,
  27. ?User $author = null,
  28. ?ProductionOrderDelivery $delivery = null,
  29. ?ProductionOrderInstallation $installation = null,
  30. ): void {
  31. $order->loadMissing(['manager', 'deliveries.driver', 'installations.brigadier']);
  32. $authorSuffix = $author ? sprintf(' Изменил %s.', $author->name) : '';
  33. $authorSuffixHtml = $author ? sprintf(' Изменил %s.', e($author->name)) : '';
  34. $url = route('schedule.orders.show', $order);
  35. $label = 'заказ №'.$order->order_number;
  36. $htmlLabel = sprintf('<a href="%s">заказ №%s</a>', $url, e($order->order_number));
  37. [$message, $messageHtml] = match ($event) {
  38. ProductionOrderNotificationEvent::Created => [
  39. sprintf('Создан %s, %s.', $label, $order->object_address).$authorSuffix,
  40. sprintf('Создан %s, %s.', $htmlLabel, e($order->object_address)).$authorSuffixHtml,
  41. ],
  42. ProductionOrderNotificationEvent::StatusChanged => [
  43. sprintf('Статус %s изменён на «%s».', $label, $order->statusLabel()).$authorSuffix,
  44. sprintf('Статус %s изменён на «%s».', $htmlLabel, e($order->statusLabel())).$authorSuffixHtml,
  45. ],
  46. ProductionOrderNotificationEvent::DeliveryAdded => [
  47. sprintf(
  48. 'Для %s добавлена доставка на %s, водитель %s.',
  49. $label,
  50. $delivery?->delivery_date?->format('d.m.Y') ?? '—',
  51. $delivery?->driver?->name ?? '—',
  52. ).$authorSuffix,
  53. sprintf(
  54. 'Для %s добавлена доставка на %s, водитель %s.',
  55. $htmlLabel,
  56. e($delivery?->delivery_date?->format('d.m.Y') ?? '—'),
  57. e($delivery?->driver?->name ?? '—'),
  58. ).$authorSuffixHtml,
  59. ],
  60. ProductionOrderNotificationEvent::InstallationAdded => [
  61. sprintf(
  62. 'Для %s добавлен монтаж на %s, бригадир %s.',
  63. $label,
  64. $installation?->installation_date?->format('d.m.Y') ?? '—',
  65. $installation?->brigadier?->name ?? '—',
  66. ).$authorSuffix,
  67. sprintf(
  68. 'Для %s добавлен монтаж на %s, бригадир %s.',
  69. $htmlLabel,
  70. e($installation?->installation_date?->format('d.m.Y') ?? '—'),
  71. e($installation?->brigadier?->name ?? '—'),
  72. ).$authorSuffixHtml,
  73. ],
  74. ProductionOrderNotificationEvent::ReclamationAdded => [
  75. sprintf('Для %s добавлена рекламация.', $label).$authorSuffix,
  76. sprintf('Для %s добавлена рекламация.', $htmlLabel).$authorSuffixHtml,
  77. ],
  78. };
  79. foreach ($this->productionOrderRecipients($order) as $user) {
  80. $settings = $this->settingsForUser($user->id);
  81. $channels = $settings->getChannelsForKey('production_order_settings', $event->value);
  82. if ($channels === []) {
  83. continue;
  84. }
  85. $notification = $this->createInAppNotification(
  86. $user,
  87. UserNotification::TYPE_PRODUCTION_ORDER,
  88. $event->value,
  89. 'График заказов',
  90. $message,
  91. $messageHtml,
  92. [
  93. 'production_order_id' => $order->id,
  94. 'delivery_id' => $delivery?->id,
  95. 'installation_id' => $installation?->id,
  96. ],
  97. );
  98. $this->dispatchDeliveryJobs($notification, [
  99. NotificationDeliveryLog::CHANNEL_BROWSER => ! empty($channels['browser']),
  100. NotificationDeliveryLog::CHANNEL_PUSH => ! empty($channels['push']),
  101. NotificationDeliveryLog::CHANNEL_EMAIL => ! empty($channels['email']),
  102. ]);
  103. }
  104. }
  105. public function notifyChatMessage(ChatMessage $chatMessage, array $recipientIds = [], bool $forceBrowserNotification = false): void
  106. {
  107. $chatMessage->loadMissing([
  108. 'user',
  109. 'targetUser',
  110. 'order.user',
  111. 'order.brigadier',
  112. 'reclamation.order',
  113. 'reclamation.productionOrder',
  114. 'reclamation.user',
  115. 'reclamation.brigadier',
  116. ]);
  117. $type = $chatMessage->order_id ? UserNotification::TYPE_PLATFORM : UserNotification::TYPE_RECLAMATION;
  118. $sourceKey = $chatMessage->order_id ? 'platform' : 'reclamation';
  119. $title = $chatMessage->order_id ? 'Чат площадки' : 'Чат рекламации';
  120. [$message, $messageHtml, $payload] = $this->buildChatNotificationContent($chatMessage, $type);
  121. foreach ($this->chatRecipients($chatMessage, $recipientIds) as $user) {
  122. $settings = $this->settingsForUser($user->id);
  123. if (!$forceBrowserNotification && !$settings->isSectionEnabled('chat_settings')) {
  124. continue;
  125. }
  126. $channels = $settings->getChannelsForKey('chat_settings', $sourceKey);
  127. if (!$forceBrowserNotification && empty($channels)) {
  128. continue;
  129. }
  130. $notification = $this->createInAppNotification(
  131. $user,
  132. $type,
  133. UserNotification::EVENT_CHAT_MESSAGE,
  134. $title,
  135. $message,
  136. $messageHtml,
  137. $payload,
  138. );
  139. $this->dispatchDeliveryJobs($notification, [
  140. NotificationDeliveryLog::CHANNEL_BROWSER => $forceBrowserNotification || !empty($channels['browser']),
  141. NotificationDeliveryLog::CHANNEL_PUSH => !empty($channels['push']),
  142. NotificationDeliveryLog::CHANNEL_EMAIL => !empty($channels['email']),
  143. ]);
  144. }
  145. }
  146. public function notifyOrderCreated(Order $order, ?User $author = null): void
  147. {
  148. $statusName = $order->orderStatus?->name ?? (Order::STATUS_NAMES[$order->order_status_id] ?? '-');
  149. $authorSuffix = $author ? sprintf(' Изменил %s.', $author->name) : '';
  150. $authorSuffixHtml = $author ? sprintf(' Изменил %s.', e($author->name)) : '';
  151. $this->notifyOrderEvent(
  152. $order,
  153. UserNotification::EVENT_CREATED,
  154. 'Площадки',
  155. sprintf('Добавлена новая площадка %s.', $order->object_address) . $authorSuffix,
  156. sprintf(
  157. 'Добавлена новая площадка <a href="%s">%s</a>.',
  158. route('order.show', ['order' => $order->id, 'sync_year' => 1]),
  159. e($order->object_address)
  160. ) . $authorSuffixHtml,
  161. $statusName,
  162. );
  163. }
  164. public function notifyOrderStatusChanged(Order $order, ?User $author = null): void
  165. {
  166. $statusName = $order->orderStatus?->name ?? (Order::STATUS_NAMES[$order->order_status_id] ?? '-');
  167. $authorSuffix = $author ? sprintf(' Изменил %s.', $author->name) : '';
  168. $authorSuffixHtml = $author ? sprintf(' Изменил %s.', e($author->name)) : '';
  169. $this->notifyOrderEvent(
  170. $order,
  171. UserNotification::EVENT_STATUS_CHANGED,
  172. 'Площадки',
  173. sprintf('Статус площадки %s изменен на %s.', $order->object_address, $statusName) . $authorSuffix,
  174. sprintf(
  175. 'Статус площадки <a href="%s">%s</a> изменен на %s.',
  176. route('order.show', ['order' => $order->id, 'sync_year' => 1]),
  177. e($order->object_address),
  178. e($statusName)
  179. ) . $authorSuffixHtml,
  180. $statusName,
  181. );
  182. }
  183. public function notifyReclamationCreated(Reclamation $reclamation, ?User $author = null): void
  184. {
  185. $order = $reclamation->order;
  186. $productionOrder = $reclamation->productionOrder;
  187. if (!$order && !$productionOrder) {
  188. return;
  189. }
  190. $authorSuffix = $author ? sprintf(' Изменил %s.', $author->name) : '';
  191. $authorSuffixHtml = $author ? sprintf(' Изменил %s.', e($author->name)) : '';
  192. $address = $order?->object_address ?? $productionOrder?->object_address ?? '—';
  193. $sourceUrl = $order
  194. ? route('order.show', ['order' => $order->id, 'sync_year' => 1])
  195. : route('schedule.orders.show', ['productionOrder' => $productionOrder]);
  196. $message = sprintf(
  197. 'Добавлена новая рекламация по адресу %s #%d.',
  198. $address,
  199. $reclamation->id,
  200. ) . $authorSuffix;
  201. $messageHtml = sprintf(
  202. 'Добавлена новая рекламация по адресу <a href="%s">%s</a> <a href="%s">#%d</a>.',
  203. $sourceUrl,
  204. e($address),
  205. route('reclamations.show', ['reclamation' => $reclamation->id]),
  206. $reclamation->id,
  207. ) . $authorSuffixHtml;
  208. $this->notifyReclamationEvent(
  209. $reclamation,
  210. UserNotification::EVENT_CREATED,
  211. 'Рекламации',
  212. $message,
  213. $messageHtml,
  214. (int)$reclamation->status_id,
  215. );
  216. }
  217. public function notifyReclamationStatusChanged(Reclamation $reclamation, ?User $author = null): void
  218. {
  219. $order = $reclamation->order;
  220. $productionOrder = $reclamation->productionOrder;
  221. if (!$order && !$productionOrder) {
  222. return;
  223. }
  224. $statusName = $reclamation->status?->name ?? (Reclamation::STATUS_NAMES[$reclamation->status_id] ?? '-');
  225. $authorSuffix = $author ? sprintf(' Изменил %s.', $author->name) : '';
  226. $authorSuffixHtml = $author ? sprintf(' Изменил %s.', e($author->name)) : '';
  227. $address = $order?->object_address ?? $productionOrder?->object_address ?? '—';
  228. $sourceUrl = $order
  229. ? route('order.show', ['order' => $order->id, 'sync_year' => 1])
  230. : route('schedule.orders.show', ['productionOrder' => $productionOrder]);
  231. $message = sprintf(
  232. 'Статус рекламации %s #%d изменен на %s.',
  233. $address,
  234. $reclamation->id,
  235. $statusName,
  236. ) . $authorSuffix;
  237. $messageHtml = sprintf(
  238. 'Статус рекламации по адресу <a href="%s">%s</a> <a href="%s">#%d</a> изменен на %s.',
  239. $sourceUrl,
  240. e($address),
  241. route('reclamations.show', ['reclamation' => $reclamation->id]),
  242. $reclamation->id,
  243. e($statusName),
  244. ) . $authorSuffixHtml;
  245. $this->notifyReclamationEvent(
  246. $reclamation,
  247. UserNotification::EVENT_STATUS_CHANGED,
  248. 'Рекламации',
  249. $message,
  250. $messageHtml,
  251. (int)$reclamation->status_id,
  252. );
  253. }
  254. public function notifyScheduleAdded(Schedule $schedule, ?User $author = null): void
  255. {
  256. $sourceKey = $this->sourceToSettingKey((string)$schedule->source);
  257. if (!$sourceKey) {
  258. return;
  259. }
  260. $brigadierName = $schedule->brigadier?->name ?? '-';
  261. $date = DateHelper::getHumanDate((string)$schedule->installation_date, true);
  262. $authorSuffix = $author ? sprintf(' Изменил %s.', $author->name) : '';
  263. $authorSuffixHtml = $author ? sprintf(' Изменил %s.', e($author->name)) : '';
  264. if ((string)$schedule->source === 'Рекламации') {
  265. $reclamationId = $this->extractReclamationId((string)$schedule->address_code);
  266. $message = sprintf(
  267. 'Рекламация №%s по адресу %s добавлена в график на %s, Бригадир %s.',
  268. $reclamationId ?? '—',
  269. $schedule->object_address,
  270. $date,
  271. $brigadierName,
  272. ) . $authorSuffix;
  273. $reclamationLink = $reclamationId
  274. ? route('reclamations.show', ['reclamation' => $reclamationId])
  275. : route('schedule.index');
  276. $orderLink = $schedule->order_id
  277. ? route('order.show', ['order' => $schedule->order_id, 'sync_year' => 1])
  278. : null;
  279. $addressHtml = $orderLink
  280. ? sprintf('<a href="%s">%s</a>', $orderLink, e($schedule->object_address))
  281. : e($schedule->object_address);
  282. $messageHtml = sprintf(
  283. '<a href="%s">Рекламация №%s</a> по адресу %s добавлена в график на %s, Бригадир %s.',
  284. $reclamationLink,
  285. $reclamationId ?? '—',
  286. $addressHtml,
  287. e($date),
  288. e($brigadierName),
  289. ) . $authorSuffixHtml;
  290. } else {
  291. $message = sprintf(
  292. '%s добавлено в график монтажей на %s, Бригадир %s.',
  293. $schedule->object_address,
  294. $date,
  295. $brigadierName,
  296. ) . $authorSuffix;
  297. $orderLink = $schedule->order_id
  298. ? route('order.show', ['order' => $schedule->order_id, 'sync_year' => 1])
  299. : route('schedule.index');
  300. $messageHtml = sprintf(
  301. '<a href="%s">%s</a> добавлено в график монтажей на %s, Бригадир %s.',
  302. $orderLink,
  303. e($schedule->object_address),
  304. e($date),
  305. e($brigadierName),
  306. ) . $authorSuffixHtml;
  307. }
  308. $users = $this->scheduleRecipients($schedule);
  309. foreach ($users as $user) {
  310. $settings = $this->settingsForUser($user->id);
  311. if (!$settings->isSectionEnabled('schedule_settings')) {
  312. continue;
  313. }
  314. $channels = $settings->getChannelsForKey('schedule_settings', $sourceKey);
  315. if (empty($channels)) {
  316. continue;
  317. }
  318. $notification = $this->createInAppNotification(
  319. $user,
  320. UserNotification::TYPE_SCHEDULE,
  321. UserNotification::EVENT_SCHEDULE_ADDED,
  322. 'График монтажей',
  323. $message,
  324. $messageHtml,
  325. [
  326. 'schedule_id' => $schedule->id,
  327. 'source' => $schedule->source,
  328. ],
  329. );
  330. $this->dispatchDeliveryJobs($notification, [
  331. NotificationDeliveryLog::CHANNEL_BROWSER => !empty($channels['browser']),
  332. NotificationDeliveryLog::CHANNEL_PUSH => !empty($channels['push']),
  333. NotificationDeliveryLog::CHANNEL_EMAIL => !empty($channels['email']),
  334. ]);
  335. }
  336. }
  337. public function deliverChannel(int $userNotificationId, string $channel, int $attempt): void
  338. {
  339. $notification = UserNotification::query()->with('user')->find($userNotificationId);
  340. if (!$notification || !$notification->user) {
  341. return;
  342. }
  343. try {
  344. if ($channel === NotificationDeliveryLog::CHANNEL_BROWSER) {
  345. event(new SendPersistentNotificationEvent($notification->user_id, [
  346. 'id' => $notification->id,
  347. 'type' => $notification->type,
  348. 'title' => $notification->title,
  349. 'message' => $notification->message,
  350. 'message_html' => $notification->message_html,
  351. 'created_at' => $notification->created_at?->toDateTimeString(),
  352. ]));
  353. $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_SENT, $attempt, null);
  354. return;
  355. }
  356. if ($channel === NotificationDeliveryLog::CHANNEL_PUSH) {
  357. if (!$notification->user->token_fcm) {
  358. $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_SKIPPED, $attempt, 'Отсутствует token_fcm');
  359. return;
  360. }
  361. $notification->user->notify(new FireBaseNotification($notification->title, Str::limit(strip_tags($notification->message), 200)));
  362. $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_SENT, $attempt, null);
  363. return;
  364. }
  365. if ($channel === NotificationDeliveryLog::CHANNEL_EMAIL) {
  366. $email = $notification->user->notification_email;
  367. if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
  368. $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_SKIPPED, $attempt, 'Отсутствует валидный notification_email');
  369. return;
  370. }
  371. Mail::html($notification->message_html, function ($message) use ($email, $notification) {
  372. $message->to($email)
  373. ->subject($notification->title);
  374. });
  375. $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_SENT, $attempt, null);
  376. return;
  377. }
  378. $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_SKIPPED, $attempt, 'Неизвестный канал');
  379. } catch (\Throwable $exception) {
  380. $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_FAILED, $attempt, $exception->getMessage());
  381. throw $exception;
  382. }
  383. }
  384. public function markDeadLetter(int $userNotificationId, string $channel, int $attempt, ?string $error = null): void
  385. {
  386. $notification = UserNotification::query()->find($userNotificationId);
  387. if (!$notification) {
  388. return;
  389. }
  390. $this->createLog($notification, $channel, NotificationDeliveryLog::STATUS_DEAD_LETTER, $attempt, $error);
  391. }
  392. private function notifyOrderEvent(
  393. Order $order,
  394. string $event,
  395. string $title,
  396. string $message,
  397. string $messageHtml,
  398. string $statusName,
  399. ): void {
  400. $users = $this->orderRecipients($order);
  401. $statusId = (int) $order->order_status_id;
  402. foreach ($users as $user) {
  403. $settings = $this->settingsForUser($user->id);
  404. if (!$settings->isSectionEnabled('order_settings')) {
  405. continue;
  406. }
  407. $channels = $settings->getChannelsForKey('order_settings', $statusId);
  408. if (empty($channels)) {
  409. continue;
  410. }
  411. $notification = $this->createInAppNotification(
  412. $user,
  413. UserNotification::TYPE_PLATFORM,
  414. $event,
  415. $title,
  416. $message,
  417. $messageHtml,
  418. [
  419. 'order_id' => $order->id,
  420. 'status' => $statusName,
  421. ],
  422. );
  423. $this->dispatchDeliveryJobs($notification, [
  424. NotificationDeliveryLog::CHANNEL_BROWSER => !empty($channels['browser']),
  425. NotificationDeliveryLog::CHANNEL_PUSH => !empty($channels['push']),
  426. NotificationDeliveryLog::CHANNEL_EMAIL => !empty($channels['email']),
  427. ]);
  428. }
  429. }
  430. private function notifyReclamationEvent(
  431. Reclamation $reclamation,
  432. string $event,
  433. string $title,
  434. string $message,
  435. string $messageHtml,
  436. int $statusId,
  437. ): void {
  438. $users = $this->reclamationRecipients($reclamation);
  439. foreach ($users as $user) {
  440. $settings = $this->settingsForUser($user->id);
  441. if (!$settings->isSectionEnabled('reclamation_settings')) {
  442. continue;
  443. }
  444. $channels = $settings->getChannelsForKey('reclamation_settings', $statusId);
  445. if (empty($channels)) {
  446. continue;
  447. }
  448. $notification = $this->createInAppNotification(
  449. $user,
  450. UserNotification::TYPE_RECLAMATION,
  451. $event,
  452. $title,
  453. $message,
  454. $messageHtml,
  455. [
  456. 'reclamation_id' => $reclamation->id,
  457. 'status_id' => $statusId,
  458. ],
  459. );
  460. $this->dispatchDeliveryJobs($notification, [
  461. NotificationDeliveryLog::CHANNEL_BROWSER => !empty($channels['browser']),
  462. NotificationDeliveryLog::CHANNEL_PUSH => !empty($channels['push']),
  463. NotificationDeliveryLog::CHANNEL_EMAIL => !empty($channels['email']),
  464. ]);
  465. }
  466. }
  467. private function dispatchDeliveryJobs(UserNotification $notification, array $channels): void
  468. {
  469. foreach ($channels as $channel => $enabled) {
  470. if (!$enabled) {
  471. continue;
  472. }
  473. SendUserNotificationChannelJob::dispatch($notification->id, $channel);
  474. }
  475. }
  476. private function createInAppNotification(
  477. User $user,
  478. string $type,
  479. string $event,
  480. string $title,
  481. string $message,
  482. string $messageHtml,
  483. array $payload,
  484. ): UserNotification {
  485. $notification = UserNotification::query()->create([
  486. 'user_id' => $user->id,
  487. 'type' => $type,
  488. 'event' => $event,
  489. 'title' => $title,
  490. 'message' => $message,
  491. 'message_html' => $messageHtml,
  492. 'data' => $payload,
  493. ]);
  494. $this->createLog($notification, NotificationDeliveryLog::CHANNEL_IN_APP, NotificationDeliveryLog::STATUS_SENT, 1, null);
  495. return $notification;
  496. }
  497. private function createLog(
  498. UserNotification $notification,
  499. string $channel,
  500. string $status,
  501. int $attempt,
  502. ?string $error,
  503. ): void {
  504. NotificationDeliveryLog::query()->create([
  505. 'user_notification_id' => $notification->id,
  506. 'user_id' => $notification->user_id,
  507. 'channel' => $channel,
  508. 'status' => $status,
  509. 'attempt' => $attempt,
  510. 'message' => $notification->message,
  511. 'error' => $error,
  512. ]);
  513. }
  514. private function settingsForUser(int $userId): UserNotificationSetting
  515. {
  516. return UserNotificationSetting::query()->firstOrCreate(
  517. ['user_id' => $userId],
  518. UserNotificationSetting::defaultsForUser($userId),
  519. );
  520. }
  521. private function orderRecipients(Order $order): Collection
  522. {
  523. $query = User::query()
  524. ->withAnyPermission(['orders.scope.admin', 'orders.scope.warehouse_head']);
  525. if ($order->user_id) {
  526. $query->orWhere('id', $order->user_id);
  527. }
  528. return $query->distinct()->get();
  529. }
  530. private function productionOrderRecipients(ProductionOrder $order): Collection
  531. {
  532. $ids = collect([$order->manager_id])
  533. ->merge($order->deliveries->pluck('driver_id'))
  534. ->merge($order->installations->pluck('brigadier_id'))
  535. ->filter()
  536. ->map(static fn ($id): int => (int) $id)
  537. ->unique()
  538. ->values();
  539. return User::query()->whereKey($ids)->get();
  540. }
  541. private function reclamationRecipients(Reclamation $reclamation): Collection
  542. {
  543. $query = User::query()
  544. ->withAnyPermission(['reclamations.scope.admin', 'reclamations.scope.warehouse_head']);
  545. $managerId = $reclamation->user_id;
  546. if ($managerId) {
  547. $query->orWhere('id', $managerId);
  548. }
  549. return $query->distinct()->get();
  550. }
  551. private function scheduleRecipients(Schedule $schedule): Collection
  552. {
  553. $query = User::query()
  554. ->withAnyPermission(['schedule.scope.admin', 'orders.scope.warehouse_head', 'reclamations.scope.warehouse_head']);
  555. if ($schedule->brigadier_id) {
  556. $query->orWhere('id', $schedule->brigadier_id);
  557. }
  558. $managerId = null;
  559. if ((string)$schedule->source === 'Площадки' && $schedule->order_id) {
  560. $managerId = Order::query()
  561. ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
  562. ->where('id', $schedule->order_id)
  563. ->value('user_id');
  564. }
  565. if ((string)$schedule->source === 'Рекламации') {
  566. $reclamationId = $this->extractReclamationId((string)$schedule->address_code);
  567. if ($reclamationId) {
  568. $reclamation = Reclamation::query()
  569. ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
  570. ->with('order')
  571. ->find($reclamationId);
  572. $managerId = $reclamation?->order?->user_id ?: $reclamation?->user_id;
  573. }
  574. }
  575. if ($managerId) {
  576. $query->orWhere('id', $managerId);
  577. }
  578. return $query->distinct()->get();
  579. }
  580. private function extractReclamationId(string $addressCode): ?int
  581. {
  582. if (preg_match('/^РЕКЛ-(\d+)$/u', $addressCode, $matches)) {
  583. return (int)$matches[1];
  584. }
  585. return null;
  586. }
  587. private function sourceToSettingKey(string $source): ?string
  588. {
  589. return match ($source) {
  590. 'Площадки' => 'platform',
  591. 'Рекламации' => 'reclamation',
  592. default => null,
  593. };
  594. }
  595. private function chatRecipients(ChatMessage $chatMessage, array $recipientIds = []): Collection
  596. {
  597. $recipientIds = array_values(array_unique(array_map(static fn ($id) => (int) $id, $recipientIds)));
  598. $recipientIds = array_values(array_diff($recipientIds, [(int) $chatMessage->user_id]));
  599. if (!empty($recipientIds)) {
  600. return User::query()
  601. ->whereIn('id', $recipientIds)
  602. ->get();
  603. }
  604. if ($chatMessage->notification_type === ChatMessage::NOTIFICATION_USER) {
  605. if (!$chatMessage->target_user_id || (int) $chatMessage->target_user_id === (int) $chatMessage->user_id) {
  606. return new Collection();
  607. }
  608. return User::query()
  609. ->where('id', $chatMessage->target_user_id)
  610. ->get();
  611. }
  612. if (!in_array($chatMessage->notification_type, [
  613. ChatMessage::NOTIFICATION_ALL,
  614. ChatMessage::NOTIFICATION_RESPONSIBLES,
  615. ], true)) {
  616. return new Collection();
  617. }
  618. $recipientIds = [];
  619. if ($chatMessage->order) {
  620. $recipientIds = $chatMessage->notification_type === ChatMessage::NOTIFICATION_ALL
  621. ? $this->allChatRecipientIds()
  622. : $this->chatResponsibleRecipientIdsForOrder($chatMessage->order);
  623. }
  624. if ($chatMessage->reclamation) {
  625. $recipientIds = $chatMessage->notification_type === ChatMessage::NOTIFICATION_ALL
  626. ? $this->allChatRecipientIds()
  627. : $this->chatResponsibleRecipientIdsForReclamation($chatMessage->reclamation);
  628. }
  629. $recipientIds = array_values(array_unique(array_filter($recipientIds)));
  630. $recipientIds = array_values(array_diff($recipientIds, [(int) $chatMessage->user_id]));
  631. if (empty($recipientIds)) {
  632. return new Collection();
  633. }
  634. return User::query()
  635. ->whereIn('id', $recipientIds)
  636. ->get();
  637. }
  638. private function allChatRecipientIds(): array
  639. {
  640. return User::query()
  641. ->pluck('id')
  642. ->map(static fn ($id) => (int) $id)
  643. ->all();
  644. }
  645. private function chatResponsibleRecipientIdsForOrder(Order $order): array
  646. {
  647. $adminIds = User::query()
  648. ->withPermission('orders.scope.admin')
  649. ->pluck('id')
  650. ->map(static fn ($id) => (int) $id)
  651. ->all();
  652. return array_merge($adminIds, [
  653. $order->user_id ? (int) $order->user_id : null,
  654. $order->brigadier_id ? (int) $order->brigadier_id : null,
  655. ]);
  656. }
  657. private function chatResponsibleRecipientIdsForReclamation(Reclamation $reclamation): array
  658. {
  659. $adminIds = User::query()
  660. ->withPermission('reclamations.scope.admin')
  661. ->pluck('id')
  662. ->map(static fn ($id) => (int) $id)
  663. ->all();
  664. return array_merge($adminIds, [
  665. $reclamation->user_id ? (int) $reclamation->user_id : null,
  666. $reclamation->brigadier_id ? (int) $reclamation->brigadier_id : null,
  667. ]);
  668. }
  669. private function buildChatNotificationContent(ChatMessage $chatMessage, string $type): array
  670. {
  671. $senderName = $chatMessage->user?->name ?? 'Пользователь';
  672. $text = trim((string) $chatMessage->message);
  673. $text = $text !== '' ? Str::limit($text, 200) : 'Вложение';
  674. if ($type === UserNotification::TYPE_PLATFORM) {
  675. $order = $chatMessage->order;
  676. $address = $order?->object_address ?? '-';
  677. $orderUrl = $order ? route('order.show', ['order' => $order->id, 'sync_year' => 1]) : route('order.index');
  678. $message = sprintf('Новое сообщение в чате площадки %s от %s: %s', $address, $senderName, $text);
  679. $messageHtml = sprintf(
  680. 'Новое сообщение в <a href="%s">чате площадки %s</a> от %s: %s',
  681. $orderUrl,
  682. e($address),
  683. e($senderName),
  684. e($text)
  685. );
  686. return [$message, $messageHtml, [
  687. 'chat_message_id' => $chatMessage->id,
  688. 'order_id' => $order?->id,
  689. ]];
  690. }
  691. $reclamation = $chatMessage->reclamation;
  692. $address = $reclamation?->order?->object_address
  693. ?? $reclamation?->productionOrder?->object_address
  694. ?? '-';
  695. $reclamationUrl = $reclamation
  696. ? route('reclamations.show', ['reclamation' => $reclamation->id, 'sync_year' => 1])
  697. : route('reclamations.index');
  698. $reclamationNumber = $reclamation?->id ? ('#' . $reclamation->id) : '#-';
  699. $message = sprintf('Новое сообщение в чате рекламации %s по адресу %s от %s: %s', $reclamationNumber, $address, $senderName, $text);
  700. $messageHtml = sprintf(
  701. 'Новое сообщение в <a href="%s">чате рекламации %s</a> по адресу %s от %s: %s',
  702. $reclamationUrl,
  703. e($reclamationNumber),
  704. e($address),
  705. e($senderName),
  706. e($text)
  707. );
  708. return [$message, $messageHtml, [
  709. 'chat_message_id' => $chatMessage->id,
  710. 'reclamation_id' => $reclamation?->id,
  711. ]];
  712. }
  713. }