Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
63.86% |
53 / 83 |
|
16.67% |
1 / 6 |
CRAP | |
0.00% |
0 / 1 |
| NotificationController | |
63.86% |
53 / 83 |
|
16.67% |
1 / 6 |
20.98 | |
0.00% |
0 / 1 |
| index | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
| create | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
| store | |
95.12% |
39 / 41 |
|
0.00% |
0 / 1 |
6 | |||
| show | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
| download | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| ensureBelongsToCurrentStore | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
3.33 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\Admin; |
| 4 | |
| 5 | use App\Enums\NotificationType; |
| 6 | use App\Http\Controllers\Concerns\ResolvesContext; |
| 7 | use App\Http\Controllers\Controller; |
| 8 | use App\Models\Influencer; |
| 9 | use App\Models\NotificationAttachment; |
| 10 | use App\Models\NotificationMessage; |
| 11 | use App\Services\NotificationService; |
| 12 | use Illuminate\Http\RedirectResponse; |
| 13 | use Illuminate\Http\Request; |
| 14 | use Illuminate\Support\Facades\Storage; |
| 15 | use Illuminate\Validation\Rule; |
| 16 | use RuntimeException; |
| 17 | use Illuminate\View\View; |
| 18 | |
| 19 | class NotificationController extends Controller |
| 20 | { |
| 21 | use ResolvesContext; |
| 22 | |
| 23 | public function index(): View |
| 24 | { |
| 25 | $store = $this->currentStore(); |
| 26 | |
| 27 | $notifications = NotificationMessage::query() |
| 28 | ->forStore($store) |
| 29 | ->with('createdBy') |
| 30 | ->withCount([ |
| 31 | 'recipients', |
| 32 | 'recipients as recipients_read_count' => fn ($query) => $query->whereNotNull('read_at'), |
| 33 | 'recipients as recipients_unread_count' => fn ($query) => $query->whereNull('read_at'), |
| 34 | 'attachments', |
| 35 | ]) |
| 36 | ->latest() |
| 37 | ->paginate(20); |
| 38 | |
| 39 | return view('admin.notifications.index', compact('notifications')); |
| 40 | } |
| 41 | |
| 42 | public function create(): View |
| 43 | { |
| 44 | $store = $this->currentStore(); |
| 45 | |
| 46 | return view('admin.notifications.create', [ |
| 47 | 'influencers' => Influencer::query() |
| 48 | ->where('tenant_id', $store->tenant_id) |
| 49 | ->where('store_id', $store->id) |
| 50 | ->where('status', 'active') |
| 51 | ->orderBy('name') |
| 52 | ->get(), |
| 53 | 'types' => NotificationType::cases(), |
| 54 | ]); |
| 55 | } |
| 56 | |
| 57 | public function store(Request $request, NotificationService $service): RedirectResponse |
| 58 | { |
| 59 | $store = $this->currentStore(); |
| 60 | $maxKb = config('notifications.attachments.max_kb', 10240); |
| 61 | |
| 62 | $data = $request->validate([ |
| 63 | 'audience' => ['required', Rule::in(['all', 'selected', 'individual'])], |
| 64 | 'type' => ['required', Rule::in(array_map(fn (NotificationType $type) => $type->value, NotificationType::cases()))], |
| 65 | 'title' => ['required', 'string', 'max:255'], |
| 66 | 'body' => ['required', 'string', 'max:10000'], |
| 67 | 'influencer_ids' => ['nullable', 'array'], |
| 68 | 'influencer_ids.*' => ['integer', 'exists:influencers,id'], |
| 69 | 'attachments' => ['nullable', 'array', 'max:5'], |
| 70 | 'attachments.*' => ['file', 'max:'.$maxKb], |
| 71 | ]); |
| 72 | |
| 73 | $attachments = $request->file('attachments', []); |
| 74 | |
| 75 | try { |
| 76 | if ($data['audience'] === 'all') { |
| 77 | $message = $service->sendToAllActiveInfluencers( |
| 78 | $store, |
| 79 | $request->user(), |
| 80 | $data['title'], |
| 81 | $data['body'], |
| 82 | $attachments, |
| 83 | NotificationType::from($data['type']), |
| 84 | ); |
| 85 | } else { |
| 86 | $ids = array_values(array_filter($data['influencer_ids'] ?? [])); |
| 87 | |
| 88 | if ($data['audience'] === 'individual' && count($ids) !== 1) { |
| 89 | return back()->withInput()->withErrors(['influencer_ids' => 'Selecione exatamente um influenciador para envio individual.']); |
| 90 | } |
| 91 | |
| 92 | if (empty($ids)) { |
| 93 | return back()->withInput()->withErrors(['influencer_ids' => 'Selecione pelo menos um influenciador.']); |
| 94 | } |
| 95 | |
| 96 | $message = $service->sendToInfluencers( |
| 97 | $store, |
| 98 | $request->user(), |
| 99 | $ids, |
| 100 | $data['title'], |
| 101 | $data['body'], |
| 102 | $attachments, |
| 103 | NotificationType::from($data['type']), |
| 104 | ); |
| 105 | } |
| 106 | } catch (RuntimeException $exception) { |
| 107 | return back()->withInput()->withErrors(['notification' => $exception->getMessage()]); |
| 108 | } |
| 109 | |
| 110 | return redirect() |
| 111 | ->route('admin.notifications.show', $message) |
| 112 | ->with('success', 'Notificação enviada com rastreabilidade de leitura.'); |
| 113 | } |
| 114 | |
| 115 | public function show(NotificationMessage $notification): View |
| 116 | { |
| 117 | $this->ensureBelongsToCurrentStore($notification); |
| 118 | |
| 119 | $notification->load([ |
| 120 | 'createdBy', |
| 121 | 'attachments', |
| 122 | 'recipients.influencer', |
| 123 | 'recipients.user', |
| 124 | ])->loadCount([ |
| 125 | 'recipients', |
| 126 | 'recipients as recipients_read_count' => fn ($query) => $query->whereNotNull('read_at'), |
| 127 | 'recipients as recipients_unread_count' => fn ($query) => $query->whereNull('read_at'), |
| 128 | ]); |
| 129 | |
| 130 | return view('admin.notifications.show', compact('notification')); |
| 131 | } |
| 132 | |
| 133 | public function download(NotificationAttachment $attachment) |
| 134 | { |
| 135 | $attachment->load('message'); |
| 136 | $this->ensureBelongsToCurrentStore($attachment->message); |
| 137 | |
| 138 | abort_unless(Storage::disk($attachment->disk)->exists($attachment->path), 404); |
| 139 | |
| 140 | return Storage::disk($attachment->disk)->download($attachment->path, $attachment->original_name); |
| 141 | } |
| 142 | |
| 143 | private function ensureBelongsToCurrentStore(NotificationMessage $notification): void |
| 144 | { |
| 145 | $store = $this->currentStore(); |
| 146 | |
| 147 | if ((int) $notification->tenant_id !== (int) $store->tenant_id || (int) $notification->store_id !== (int) $store->id) { |
| 148 | abort(404); |
| 149 | } |
| 150 | } |
| 151 | } |