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,
};
}
}