278 lines
11 KiB
PHP
278 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Modules\AssetExchange\Services;
|
|
|
|
use App\Business;
|
|
use App\Contact;
|
|
use App\User;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Modules\AssetExchange\Models\Notification;
|
|
use Modules\AssetExchange\Models\PriceInquiry;
|
|
|
|
class AssetExchangeNotificationService
|
|
{
|
|
public function notify(
|
|
Business $business,
|
|
string $type,
|
|
string $title,
|
|
string $message,
|
|
?User $user = null,
|
|
?int $contactId = null,
|
|
?array $data = null,
|
|
string $priority = 'normal',
|
|
int $escalationLevel = 0
|
|
): ?Notification {
|
|
if (! Schema::hasTable('aex_notifications')) {
|
|
return null;
|
|
}
|
|
|
|
$notification = Notification::create([
|
|
'business_id' => $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;
|
|
}
|
|
}
|
|
|