$business->id, 'user_id' => $user?->id, 'contact_id' => $contactId, 'type' => $type, 'priority' => $priority, 'escalation_level' => $escalationLevel, 'title' => $title, 'message' => $message, 'data' => $data, 'channels' => ['in_app'], 'scheduled_at' => now(), 'sent_at' => now(), 'escalated_at' => $escalationLevel > 0 ? now() : null, ]); try { $recipientEmail = $user?->email; if (! $recipientEmail && $contactId) { $recipientEmail = Contact::where('business_id', $business->id)->where('id', $contactId)->value('email'); } if (! empty($recipientEmail) && ! empty(config('mail.default'))) { Mail::raw($message, function ($mail) use ($recipientEmail, $title) { $mail->to($recipientEmail)->subject($title); }); } } catch (\Throwable) { // Keep notification flow non-blocking when mail is unavailable. } return $notification; } public function notifyInquiryRecipients(Business $business, PriceInquiry $inquiry): Collection { $sent = collect(); $inquiry->loadMissing('listing', 'recipients'); foreach ($inquiry->recipients as $recipient) { if ($recipient->recipient_type === 'supplier' && $recipient->contact_id) { $users = User::where('business_id', $business->id) ->where('crm_contact_id', $recipient->contact_id) ->get(); if ($users->isEmpty()) { $sent->push($this->notify( $business, 'aex_inquiry_assigned', 'استعلام قیمت جدید', 'یک استعلام قیمت جدید برای شما ثبت شده است: '.$inquiry->inquiry_code, null, $recipient->contact_id, [ 'inquiry_id' => $inquiry->id, 'url' => route('supplier.asset_exchange.inquiries.show', [$inquiry->id]), ], 'high' )); } else { foreach ($users as $user) { $sent->push($this->notify( $business, 'aex_inquiry_assigned', 'استعلام قیمت جدید', 'یک استعلام قیمت جدید برای شما ثبت شده است: '.$inquiry->inquiry_code, $user, $recipient->contact_id, [ 'inquiry_id' => $inquiry->id, 'url' => route('supplier.asset_exchange.inquiries.show', [$inquiry->id]), ], 'high' )); } } } if ($recipient->recipient_type === 'internal_user' && $recipient->user_id) { $user = User::where('business_id', $business->id)->find($recipient->user_id); if ($user) { $sent->push($this->notify( $business, 'aex_internal_research_assigned', 'وظیفه استعلام/تحقیق داخلی', 'برای شما یک استعلام/تحقیق داخلی در AssetExchange اختصاص یافت: '.$inquiry->inquiry_code, $user, null, [ 'inquiry_id' => $inquiry->id, 'url' => action([\Modules\AssetExchange\Http\Controllers\PriceInquiryController::class, 'show'], [$inquiry->id]), ], 'high' )); } } } return $sent->filter(); } public function escalateOverdueItems(Business $business): int { $count = 0; if (\Illuminate\Support\Facades\Schema::hasTable('aex_price_inquiries')) { $slaService = app(InquirySlaService::class); $sla = $slaService->slaSettings($business->id); $inquiries = PriceInquiry::where('business_id', $business->id) ->whereIn('status', ['sent', 'partial']) ->with(['listing', 'recipients']) ->get(); foreach ($inquiries as $inq) { $slaStatus = $slaService->inquirySlaStatus($inq, $sla); if (! in_array($slaStatus, ['breached', 'overdue'], true)) { continue; } $exists = Notification::where('business_id', $business->id) ->where('type', 'aex_inquiry_overdue') ->where('data->inquiry_id', $inq->id) ->where('escalation_level', '>=', 1) ->exists(); if ($exists) { continue; } $message = $slaStatus === 'breached' ? 'استعلام '.$inq->inquiry_code.' از مهلت پاسخ SLA عبور کرده و هنوز پاسخ تأمین‌کننده ثبت نشده است.' : 'استعلام '.$inq->inquiry_code.' از موعد پاسخ عبور کرده است.'; $this->notify( $business, 'aex_inquiry_overdue', 'استعلام قیمت دیرکرد دارد', $message, null, null, [ 'inquiry_id' => $inq->id, 'sla_status' => $slaStatus, 'url' => action([\Modules\AssetExchange\Http\Controllers\PriceInquiryController::class, 'show'], [$inq->id]), ], 'critical', 1 ); $count++; } } if (\Illuminate\Support\Facades\Schema::hasTable('aex_research_tasks')) { $overdueTasks = \Modules\AssetExchange\Models\ResearchTask::where('business_id', $business->id) ->whereNotIn('status', ['completed', 'cancelled']) ->whereDate('due_date', '<', now()->toDateString()) ->with('researchCase') ->get(); foreach ($overdueTasks as $task) { $exists = Notification::where('business_id', $business->id) ->where('type', 'aex_research_task_overdue') ->where('data->research_task_id', $task->id) ->where('escalation_level', '>=', 1) ->exists(); if ($exists) { continue; } $this->notify( $business, 'aex_research_task_overdue', 'وظیفه تحقیق عقب‌افتاده', 'وظیفه «'.$task->title.'» از موعد انجام عبور کرده است.', $task->assignee, null, [ 'research_task_id' => $task->id, 'research_case_id' => $task->research_case_id, 'url' => action([\Modules\AssetExchange\Http\Controllers\ResearchCaseController::class, 'workspace'], [$task->research_case_id]), ], 'high', 1 ); $count++; } } return $count; } public function dailyDigest(Business $business): array { $summary = [ 'overdue_inquiries' => 0, 'pending_inquiries' => 0, 'overdue_tasks' => 0, 'open_research_cases' => 0, ]; if (\Illuminate\Support\Facades\Schema::hasTable('aex_price_inquiries')) { $slaService = app(InquirySlaService::class); $slaSnapshot = $slaService->snapshot($business); $summary['overdue_inquiries'] = ($slaSnapshot['inquiry_breached'] ?? 0) + ($slaSnapshot['inquiry_overdue_due'] ?? 0); $summary['pending_inquiries'] = PriceInquiry::where('business_id', $business->id) ->whereIn('status', ['draft', 'sent', 'partial']) ->count(); } if (\Illuminate\Support\Facades\Schema::hasTable('aex_research_tasks')) { $summary['overdue_tasks'] = \Modules\AssetExchange\Models\ResearchTask::where('business_id', $business->id) ->whereNotIn('status', ['completed', 'cancelled']) ->whereDate('due_date', '<', now()->toDateString()) ->count(); } if (\Illuminate\Support\Facades\Schema::hasTable('aex_research_cases')) { $summary['open_research_cases'] = \Modules\AssetExchange\Models\ResearchCase::where('business_id', $business->id) ->whereIn('status', ['open', 'in_progress', 'review']) ->count(); } $message = 'خلاصه روزانه AssetExchange — ' .'استعلام معوق: '.$summary['overdue_inquiries'] .' | استعلام باز: '.$summary['pending_inquiries'] .' | وظیفه تحقیق معوق: '.$summary['overdue_tasks'] .' | پرونده تحقیق باز: '.$summary['open_research_cases']; $admins = User::where('business_id', $business->id)->where('is_admin', 1)->get(); foreach ($admins as $admin) { $this->notify( $business, 'aex_daily_digest', 'گزارش روزانه AssetExchange', $message, $admin, null, ['summary' => $summary, 'url' => action([\Modules\AssetExchange\Http\Controllers\DashboardController::class, 'index'])], 'normal' ); } return $summary; } }