Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
63.86% covered (warning)
63.86%
53 / 83
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
NotificationController
63.86% covered (warning)
63.86%
53 / 83
16.67% covered (danger)
16.67%
1 / 6
20.98
0.00% covered (danger)
0.00%
0 / 1
 index
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 create
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 store
95.12% covered (success)
95.12%
39 / 41
0.00% covered (danger)
0.00%
0 / 1
6
 show
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 download
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 ensureBelongsToCurrentStore
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
1<?php
2
3namespace App\Http\Controllers\Admin;
4
5use App\Enums\NotificationType;
6use App\Http\Controllers\Concerns\ResolvesContext;
7use App\Http\Controllers\Controller;
8use App\Models\Influencer;
9use App\Models\NotificationAttachment;
10use App\Models\NotificationMessage;
11use App\Services\NotificationService;
12use Illuminate\Http\RedirectResponse;
13use Illuminate\Http\Request;
14use Illuminate\Support\Facades\Storage;
15use Illuminate\Validation\Rule;
16use RuntimeException;
17use Illuminate\View\View;
18
19class 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}