Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.13% covered (success)
98.13%
105 / 107
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
NotificationService
98.13% covered (success)
98.13%
105 / 107
83.33% covered (warning)
83.33%
5 / 6
20
0.00% covered (danger)
0.00%
0 / 1
 send
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
5
 sendToInfluencers
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 sendToAllActiveInfluencers
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 notifyPaymentPaid
95.00% covered (success)
95.00%
38 / 40
0.00% covered (danger)
0.00%
0 / 1
7
 storeAttachment
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 normalizeRecipients
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Services;
4
5use App\Enums\NotificationAudience;
6use App\Enums\NotificationRecipientStatus;
7use App\Enums\NotificationStatus;
8use App\Enums\NotificationType;
9use App\Models\Influencer;
10use App\Models\NotificationAttachment;
11use App\Models\NotificationMessage;
12use App\Models\PaymentRecord;
13use App\Models\Store;
14use App\Models\User;
15use Illuminate\Http\UploadedFile;
16use Illuminate\Support\Collection;
17use Illuminate\Support\Facades\DB;
18use Illuminate\Support\Facades\Storage;
19use Illuminate\Support\Str;
20use RuntimeException;
21
22class NotificationService
23{
24    /**
25     * @param iterable<Influencer> $recipients
26     * @param array<int, UploadedFile> $attachments
27     */
28    public function send(
29        Store $store,
30        ?User $sender,
31        string $title,
32        string $body,
33        iterable $recipients,
34        NotificationAudience $audience = NotificationAudience::Selected,
35        NotificationType $type = NotificationType::Manual,
36        array $attachments = [],
37        array $metadata = []
38    ): NotificationMessage {
39        $recipientCollection = $this->normalizeRecipients($store, $recipients);
40
41        if ($recipientCollection->isEmpty()) {
42            throw new RuntimeException('Nenhum influenciador válido foi selecionado para receber a notificação.');
43        }
44
45        return DB::transaction(function () use ($store, $sender, $title, $body, $recipientCollection, $audience, $type, $attachments, $metadata) {
46            $message = NotificationMessage::create([
47                'tenant_id' => $store->tenant_id,
48                'store_id' => $store->id,
49                'created_by_user_id' => $sender?->id,
50                'type' => $type,
51                'audience' => $audience,
52                'title' => $title,
53                'body' => $body,
54                'status' => NotificationStatus::Sent,
55                'sent_at' => now(),
56                'metadata' => $metadata,
57            ]);
58
59            foreach ($recipientCollection as $influencer) {
60                $message->recipients()->create([
61                    'tenant_id' => $store->tenant_id,
62                    'store_id' => $store->id,
63                    'influencer_id' => $influencer->id,
64                    'user_id' => $influencer->user_id,
65                    'status' => NotificationRecipientStatus::Delivered,
66                    'received_at' => now(),
67                ]);
68            }
69
70            foreach ($attachments as $attachment) {
71                if ($attachment instanceof UploadedFile) {
72                    $this->storeAttachment($message, $attachment, $sender);
73                }
74            }
75
76            return $message->fresh(['recipients', 'attachments']);
77        });
78    }
79
80    /** @param array<int, UploadedFile> $attachments */
81    public function sendToInfluencers(
82        Store $store,
83        ?User $sender,
84        array $influencerIds,
85        string $title,
86        string $body,
87        array $attachments = [],
88        NotificationType $type = NotificationType::Manual,
89        array $metadata = []
90    ): NotificationMessage {
91        $influencers = Influencer::query()
92            ->where('tenant_id', $store->tenant_id)
93            ->where('store_id', $store->id)
94            ->whereIn('id', $influencerIds)
95            ->get();
96
97        $audience = $influencers->count() === 1
98            ? NotificationAudience::Individual
99            : NotificationAudience::Selected;
100
101        return $this->send($store, $sender, $title, $body, $influencers, $audience, $type, $attachments, $metadata);
102    }
103
104    /** @param array<int, UploadedFile> $attachments */
105    public function sendToAllActiveInfluencers(
106        Store $store,
107        ?User $sender,
108        string $title,
109        string $body,
110        array $attachments = [],
111        NotificationType $type = NotificationType::Manual,
112        array $metadata = []
113    ): NotificationMessage {
114        $influencers = Influencer::query()
115            ->where('tenant_id', $store->tenant_id)
116            ->where('store_id', $store->id)
117            ->where('status', 'active')
118            ->get();
119
120        return $this->send($store, $sender, $title, $body, $influencers, NotificationAudience::All, $type, $attachments, $metadata);
121    }
122
123    public function notifyPaymentPaid(PaymentRecord $paymentRecord): ?NotificationMessage
124    {
125        $paymentRecord->loadMissing(['influencer', 'store']);
126
127        if (! $paymentRecord->influencer || ! $paymentRecord->store) {
128            return null;
129        }
130
131        $alreadySent = NotificationMessage::query()
132            ->where('tenant_id', $paymentRecord->tenant_id)
133            ->where('store_id', $paymentRecord->store_id)
134            ->where('type', NotificationType::Payment->value)
135            ->where('metadata->payment_record_id', $paymentRecord->id)
136            ->exists();
137
138        if ($alreadySent) {
139            return null;
140        }
141
142        $period = null;
143        if ($paymentRecord->settlement) {
144            $period = str_pad((string) $paymentRecord->settlement->period_month, 2, '0', STR_PAD_LEFT)
145                . '/' . $paymentRecord->settlement->period_year;
146        }
147
148        $amount = number_format((float) $paymentRecord->amount, 2, ',', '.');
149        $paidAt = $paymentRecord->paid_at?->format('d/m/Y H:i') ?? now()->format('d/m/Y H:i');
150
151        $title = 'Pagamento confirmado';
152        $body = "Olá, {$paymentRecord->influencer->name}. Seu pagamento no valor de R$ {$amount}";
153        $body .= $period ? " referente ao fechamento de {$period}" : '';
154        $body .= " foi marcado como pago em {$paidAt}.";
155        if (filled($paymentRecord->reference)) {
156            $body .= "\n\nReferência: {$paymentRecord->reference}";
157        }
158
159        return $this->send(
160            $paymentRecord->store,
161            $paymentRecord->paidBy,
162            $title,
163            $body,
164            [$paymentRecord->influencer],
165            NotificationAudience::Individual,
166            NotificationType::Payment,
167            [],
168            [
169                'payment_record_id' => $paymentRecord->id,
170                'settlement_id' => $paymentRecord->settlement_id,
171                'settlement_item_id' => $paymentRecord->settlement_item_id,
172                'amount' => (float) $paymentRecord->amount,
173                'reference' => $paymentRecord->reference,
174            ]
175        );
176    }
177
178    private function storeAttachment(NotificationMessage $message, UploadedFile $file, ?User $sender): NotificationAttachment
179    {
180        $disk = config('notifications.attachments.disk', 'local');
181        $directory = 'notifications/'.$message->tenant_id.'/'.$message->id;
182        $safeName = Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME)) ?: 'arquivo';
183        $extension = $file->getClientOriginalExtension();
184        $filename = $safeName.'-'.Str::random(10).($extension ? '.'.$extension : '');
185        $path = $file->storeAs($directory, $filename, $disk);
186
187        return $message->attachments()->create([
188            'tenant_id' => $message->tenant_id,
189            'store_id' => $message->store_id,
190            'uploaded_by_user_id' => $sender?->id,
191            'disk' => $disk,
192            'path' => $path,
193            'original_name' => $file->getClientOriginalName(),
194            'mime_type' => $file->getClientMimeType(),
195            'size_bytes' => $file->getSize() ?: 0,
196        ]);
197    }
198
199    /** @param iterable<Influencer> $recipients */
200    private function normalizeRecipients(Store $store, iterable $recipients): Collection
201    {
202        return collect($recipients)
203            ->filter(fn ($influencer) => $influencer instanceof Influencer)
204            ->filter(fn (Influencer $influencer) => (int) $influencer->tenant_id === (int) $store->tenant_id)
205            ->filter(fn (Influencer $influencer) => (int) $influencer->store_id === (int) $store->id)
206            ->unique('id')
207            ->values();
208    }
209}