Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 356
0.00% covered (danger)
0.00%
0 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
SenderBulkController
0.00% covered (danger)
0.00%
0 / 356
0.00% covered (danger)
0.00%
0 / 18
4290
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 index
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 send
0.00% covered (danger)
0.00%
0 / 95
0.00% covered (danger)
0.00%
0 / 1
306
 preview
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
12
 estimateSegment
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 saveDraft
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
30
 drafts
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 showDraft
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 editDraft
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
20
 updateDraft
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
 deleteDraft
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 sent
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 viewSent
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 resend
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 resolveContacts
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 resolveMessage
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 buildSchedule
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 resolveRecipient
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Modules\SchedulerModule\Controllers;
4
5use App\Controllers\BaseController;
6use App\Modules\ContactModule\Models\ContactModel;
7use App\Modules\NotificationModule\Models\NotificationQueueModel;
8use App\Modules\NotificationModule\Models\NotificationDraftModel;
9use App\Modules\TemplateModule\Models\MessageTemplateModel;
10use App\Modules\SecurityModule\Services\AuthService;
11use App\Modules\ContactModule\Models\SenderModel;
12use App\Modules\ChannelModule\Models\ChannelProviderModel;
13use App\Modules\TemplateModule\Services\MessageTemplateService;
14
15class SenderBulkController extends BaseController
16{
17    protected ContactModel $contactModel;
18    protected NotificationQueueModel $queueModel;
19    protected NotificationDraftModel $draftModel;
20    protected MessageTemplateModel $templateModel;
21    protected AuthService $authService;
22    protected SenderModel $senderModel;
23    protected ChannelProviderModel $providerModel;
24    protected MessageTemplateService $templateService;
25
26    public function __construct()
27    {
28        $this->contactModel  = new ContactModel();
29        $this->queueModel    = new NotificationQueueModel();
30        $this->draftModel    = new NotificationDraftModel();
31        $this->templateModel = new MessageTemplateModel();
32        $this->authService   = new AuthService();
33        $this->senderModel   = new SenderModel();
34        $this->providerModel = new ChannelProviderModel();
35        $this->templateService = service('messageTemplateService');
36    }
37
38    /* =========================
39     * VIEW
40     * ========================= */
41
42    public function index()
43    {
44        return view('App\Modules\SchedulerModule\Views\sender_bulk\index', [
45            'title' => 'Envoi de masse'
46        ]);
47    }
48
49    /* =========================
50     * SEND
51     * ========================= */
52
53    public function send()
54    {
55        try {
56            $raw  = $this->request->getBody();
57            $data = $this->request->getJSON(true);
58
59            if (!$data) {
60                return $this->response->setJSON([
61                    'status'  => 'error',
62                    'message' => 'Payload invalide — body: ' . $raw
63                ]);
64            }
65
66            // ── 1. Extraction des variables ──────────────────────────
67            $channel      = $data['channel']      ?? null;
68            $scheduleType = $data['schedule_type'] ?? 'now';
69            $senderId     = $data['sender_id']     ?? null;
70            $priority     = $data['priority']      ?? 'normal';
71
72            // ── 2. Validation des paramètres obligatoires ────────────
73            if (!$channel) {
74                return $this->response->setJSON([
75                    'status'  => 'error',
76                    'message' => 'Paramètres invalides',
77                    'debug'   => ['channel' => $channel]
78                ]);
79            }
80
81            // ── 3. Résolution des contacts ───────────────────────────
82            $contacts = $this->resolveContacts($data);
83
84            if (!$contacts) {
85                return $this->response->setJSON([
86                    'status'  => 'error',
87                    'message' => 'Aucun destinataire trouvé'
88                ]);
89            }
90
91            // ── 4. Résolution du provider ────────────────────────────
92            $db             = \Config\Database::connect();
93            $providerRecord = $db->table('channel_provider')
94                                 ->where('id', $senderId)
95                                 ->get()
96                                 ->getRowObject();
97
98            $provider = $providerRecord?->name ?? 'default';
99
100            // ── 5. Résolution du message et de la planification ──────
101            $scheduledAt = $this->buildSchedule($data);
102            $status      = ($scheduleType === 'now') ? 'processing' : 'pending';
103
104            // ── 6. Push en queue par contact ─────────────────────────
105            $queued  = 0;
106            $skipped = 0;
107            $queueItems = [];
108
109            foreach ($contacts as $contact) {
110                $recipient = $this->resolveRecipient($contact, $channel);
111
112                if (!$recipient) {
113                    $skipped++;
114                    continue;
115                }
116
117                $message = $this->resolveMessage($data, $contact);
118
119                $queueItem = service('notificationQueueFactory')->push(
120                    $contact,
121                    $channel,
122                    $message,
123                    $status,
124                    $scheduledAt,
125                    'bulk_send',
126                    null,
127                    ['sender_id' => $senderId]
128                );
129
130                if ($queueItem) {
131                    $queueItems[] = $queueItem;
132                    $queued++;
133                } else {
134                    $skipped++;
135                }
136            }
137
138            if ($queued === 0) {
139                return $this->response->setJSON([
140                    'status'  => 'error',
141                    'message' => 'Aucun contact valide trouvé',
142                    'queued'  => 0,
143                    'skipped' => $skipped,
144                ]);
145            }
146
147            // ── 7. Envoi immédiat ────────────────────────────────────
148            if ($scheduleType === 'now') {
149                $dispatchService = service('notificationDispatchService');
150                $dispatched = 0;
151                $failed     = 0;
152
153                foreach ($queueItems as $queueItem) {
154                    try {
155                        $result = $dispatchService->dispatch($queueItem);
156                        $result ? $dispatched++ : $failed++;
157                    } catch (\Throwable $dispatchEx) {
158                        log_message('error', '[BulkSend] Dispatch error: ' . $dispatchEx->getMessage());
159                        $failed++;
160                    }
161                }
162
163                return $this->response->setJSON([
164                    'status'     => $dispatched > 0 ? 'success' : 'error',
165                    'message'    => $dispatched > 0
166                        ? "{$dispatched} message(s) envoyé(s)" . ($failed > 0 ? "{$failed} échec(s)" : '')
167                        : 'Échec de tous les envois',
168                    'queued'     => $queued,
169                    'dispatched' => $dispatched,
170                    'failed'     => $failed,
171                    'skipped'    => $skipped,
172                ]);
173            }
174
175            // ── 8. Envoi planifié ────────────────────────────────────
176            return $this->response->setJSON([
177                'status'  => 'success',
178                'message' => "{$queued} notification(s) planifiée(s) pour le " . $scheduledAt,
179                'queued'  => $queued,
180                'skipped' => $skipped,
181            ]);
182
183        } catch (\Throwable $e) {
184            return $this->response->setJSON([
185                'status'  => 'error',
186                'message' => $e->getMessage(),
187                'file'    => $e->getFile(),
188                'line'    => $e->getLine(),
189                'trace'   => explode("\n", $e->getTraceAsString()),
190            ]);
191        }
192    }
193
194    /* =========================
195     * PREVIEW
196     * ========================= */
197
198    public function preview()
199    {
200        $data     = $this->request->getJSON(true) ?? $this->request->getPost();
201        $contacts = $this->resolveContacts($data);
202        $channel  = $data['channel'] ?? null;
203
204        $recipients = [];
205        $skipped    = 0;
206
207        foreach ($contacts as $contact) {
208            $recipient = $this->resolveRecipient($contact, $channel);
209
210            if (!$recipient) {
211                $skipped++;
212                continue;
213            }
214
215            $recipients[] = [
216                'id'        => $contact->id,
217                'name'      => $contact->name,
218                'recipient' => $recipient,
219                'message'   => $this->resolveMessage($data, $contact),
220            ];
221        }
222
223        return $this->response->setJSON([
224            'status'  => 'success',
225            'channel' => $channel,
226            'count'   => count($recipients),
227            'skipped' => $skipped,
228            'preview' => array_slice($recipients, 0, 5), // Aperçu des 5 premiers
229        ]);
230    }
231
232    /* =========================
233     * SEGMENT ESTIMATION
234     * ========================= */
235
236    public function estimateSegment()
237    {
238        $data     = $this->request->getJSON(true) ?? $this->request->getPost();
239        $contacts = $this->resolveContacts($data);
240        $channel  = $data['channel'] ?? null;
241
242        $valid   = 0;
243        $skipped = 0;
244
245        foreach ($contacts as $contact) {
246            $this->resolveRecipient($contact, $channel) ? $valid++ : $skipped++;
247        }
248
249        return $this->response->setJSON([
250            'status'  => 'success',
251            'total'   => count($contacts),
252            'valid'   => $valid,
253            'skipped' => $skipped,
254        ]);
255    }
256
257    /* =========================
258     * SAVE DRAFT
259     * ========================= */
260
261    public function saveDraft()
262    {
263        $data = $this->request->getJSON(true) ?? $this->request->getPost();
264
265        if (!$data) {
266            return $this->response->setJSON([
267                'status'  => 'error',
268                'message' => 'Payload invalide'
269            ]);
270        }
271
272        $userId  = $this->authService->userId();
273        $channel = $data['channel'] ?? null;
274        $message = trim($data['message'] ?? '');
275
276        if (!$userId) {
277            return $this->response->setJSON([
278                'status'  => 'error',
279                'message' => 'Utilisateur non authentifié'
280            ]);
281        }
282
283        if (!$channel || $message === '') {
284            return $this->response->setJSON([
285                'status'  => 'error',
286                'message' => 'Paramètres obligatoires manquants'
287            ]);
288        }
289
290        $scheduledAt = $this->buildSchedule($data);
291
292        $payload = [
293            'channel'      => $channel,
294            'message'      => $message,
295            'segment'      => $data['segment']     ?? 'all',
296            'sender_id'    => $data['sender_id']   ?? null,
297            'template_id'  => $data['template_id'] ?? null,
298            'scheduled_at' => $scheduledAt,
299        ];
300
301        $draftId = $this->draftModel->insert([
302            'user_id'      => $userId,
303            'channel'      => $channel,
304            'sender_id'    => $data['sender_id']   ?? null,
305            'message'      => $message,
306            'payload'      => json_encode($payload),
307            'scheduled_at' => $scheduledAt,
308            'status'       => 'draft',
309        ]);
310
311        return $this->response->setJSON([
312            'status'   => 'success',
313            'draft_id' => $draftId,
314            'message'  => 'Brouillon enregistré avec succès'
315        ]);
316    }
317
318    /* =========================
319     * DRAFT LIST
320     * ========================= */
321
322    public function drafts()
323    {
324        $userId = $this->authService->userId();
325
326        if (!$userId) {
327            return redirect()->to('/login');
328        }
329
330        $drafts = $this->draftModel
331            ->where('user_id', $userId)
332            ->where('status', 'draft')
333            ->orderBy('created_at', 'DESC')
334            ->findAll();
335
336        return view('App\Modules\SchedulerModule\Views\sender_bulk\drafts', [
337            'title'  => 'Brouillons',
338            'drafts' => $drafts
339        ]);
340    }
341
342    /* =========================
343     * SHOW DRAFT (API)
344     * ========================= */
345
346    public function showDraft($id)
347    {
348        $draft = $this->draftModel->find($id);
349
350        if (!$draft) {
351            return $this->response->setJSON([
352                'status'  => 'error',
353                'message' => 'Brouillon introuvable'
354            ]);
355        }
356
357        return $this->response->setJSON([
358            'status' => 'success',
359            'data'   => [
360                'id'         => $draft->id,
361                'channel'    => $draft->channel,
362                'message'    => $draft->message,
363                'payload'    => is_string($draft->payload)
364                                ? json_decode($draft->payload, true)
365                                : $draft->payload,
366                'created_at' => $draft->created_at,
367            ]
368        ]);
369    }
370
371    /* =========================
372     * EDIT DRAFT (API)
373     * ========================= */
374
375    public function editDraft($id)
376    {
377        $draft = $this->draftModel->find($id);
378
379        if (!$draft) {
380            return $this->response->setJSON([
381                'status'  => 'error',
382                'message' => 'Brouillon introuvable'
383            ]);
384        }
385
386        $payload = $draft->payload;
387
388        if (is_string($payload)) {
389            $payload = json_decode($payload, true);
390        } elseif (is_object($payload)) {
391            $payload = json_decode(json_encode($payload), true);
392        }
393
394        return $this->response->setJSON([
395            'status' => 'success',
396            'data'   => [
397                'id'           => $draft->id,
398                'channel'      => $draft->channel,
399                'sender_id'    => $draft->sender_id,
400                'message'      => $draft->message ?? ($payload['message'] ?? ''),
401                'segment'      => $payload['segment']     ?? 'all',
402                'template_id'  => $payload['template_id'] ?? null,
403                'scheduled_at' => $draft->scheduled_at    ?? ($payload['scheduled_at'] ?? null),
404                'payload'      => $payload,
405            ]
406        ]);
407    }
408
409    /* =========================
410     * UPDATE DRAFT
411     * ========================= */
412
413    public function updateDraft($id)
414    {
415        $data  = $this->request->getJSON(true);
416        $draft = $this->draftModel->find($id);
417
418        if (!$draft) {
419            return $this->response->setJSON([
420                'status'  => 'error',
421                'message' => 'Brouillon introuvable'
422            ]);
423        }
424
425        $scheduledAt = null;
426
427        if (!empty($data['scheduled_date']) && !empty($data['scheduled_time'])) {
428            $scheduledAt = date(
429                'Y-m-d H:i:s',
430                strtotime($data['scheduled_date'] . ' ' . $data['scheduled_time'])
431            );
432        }
433
434        $this->draftModel->update($id, [
435            'channel'      => $data['channel']   ?? $draft->channel,
436            'sender_id'    => $data['sender_id'] ?? $draft->sender_id,
437            'message'      => $data['message']   ?? $draft->message,
438            'scheduled_at' => $scheduledAt       ?? $draft->scheduled_at,
439        ]);
440
441        return $this->response->setJSON([
442            'status'  => 'success',
443            'message' => 'Brouillon mis à jour'
444        ]);
445    }
446
447    /* =========================
448     * DELETE DRAFT
449     * ========================= */
450
451    public function deleteDraft($id)
452    {
453        $draft = $this->draftModel->find($id);
454
455        if (!$draft) {
456            return $this->response->setJSON([
457                'status'  => 'error',
458                'message' => 'Brouillon introuvable'
459            ]);
460        }
461
462        $this->draftModel->delete($id);
463
464        return $this->response->setJSON([
465            'status'  => 'success',
466            'message' => 'Brouillon supprimé'
467        ]);
468    }
469
470    /* =========================
471     * SENT LIST
472     * ========================= */
473
474    public function sent()
475    {
476        return view(
477            'App\Modules\SchedulerModule\Views\sender_bulk\sent',
478            [
479                'title' => 'Historique des envois de masse',
480                'sent'  => $this->queueModel->getSentMessages('bulk_send')
481            ]
482        );
483    }
484
485    /* =========================
486     * VIEW SENT (détail)
487     * ========================= */
488
489    public function viewSent($id)
490    {
491        $notification = $this->queueModel->find($id);
492
493        if (!$notification) {
494            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
495        }
496
497        $payload = $notification->payload;
498
499        if (is_string($payload)) {
500            $decoded = json_decode($payload, true);
501            $payload = json_last_error() === JSON_ERROR_NONE ? $decoded : [];
502        } elseif (is_object($payload)) {
503            $payload = json_decode(json_encode($payload), true);
504        }
505
506        return view(
507            'App\Modules\SchedulerModule\Views\sender_bulk\view_sent',
508            [
509                'notification' => $notification,
510                'payload'      => $payload,
511            ]
512        );
513    }
514
515    /* =========================
516     * RESEND
517     * ========================= */
518
519    public function resend($id)
520    {
521        $old = $this->queueModel->find($id);
522
523        if (!$old) {
524            return redirect()->back()->with('error', 'Message introuvable');
525        }
526
527        $contact = $this->contactModel->find($old->contact_id);
528
529        if (!$contact) {
530            return redirect()->back()->with('error', 'Contact introuvable');
531        }
532
533        $this->queueModel->insert([
534            'contact_id'   => $old->contact_id,
535            'channel'      => $old->channel,
536            'payload'      => $old->payload,
537            'status'       => 'pending',
538            'attempts'     => 0,
539            'scheduled_at' => date('Y-m-d H:i:s'),
540            'meta_key'     => 'resend_from_' . $old->id,
541        ]);
542
543        return redirect()
544            ->back()
545            ->with('success',
546                'Message reprogrammé pour ' .
547                ($contact->name  ?? 'N/A') .
548                ' (' .
549                ($contact->email ?? 'N/A') .
550                ')'
551            );
552    }
553
554    /* =========================
555     * CONTACT RESOLUTION
556     * ========================= */
557
558    private function resolveContacts(array $data): array
559    {
560        $segment = $data['segment'] ?? 'all';
561        $builder = $this->contactModel->where('status', 'active');
562
563        switch ($segment) {
564            case 'clients_active':
565                $builder->where('type', 'client')->where('is_active', 1);
566                break;
567
568            case 'clients_inactive':
569                $builder->where('type', 'client')->where('is_active', 0);
570                break;
571
572            case 'prospects':
573                $builder->where('type', 'prospect');
574                break;
575        }
576
577        return $builder->findAll();
578    }
579
580    /* =========================
581     * HELPERS
582     * ========================= */
583
584    private function resolveMessage(array $data, $contact): string
585    {
586        $templateId = $data['template_id'] ?? null;
587
588        if ($templateId) {
589            $template = $this->templateModel->find($templateId);
590
591            if ($template) {
592                return $this->templateService->render(
593                    $template->content,
594                    [
595                        'name'    => $contact->name,
596                        'email'   => $contact->email   ?? '',
597                        'phone'   => $contact->phone   ?? '',
598                        'company' => $contact->company ?? '',
599                    ]
600                );
601            }
602        }
603
604        return trim($data['message'] ?? '');
605    }
606
607    private function buildSchedule(array $data): ?string
608    {
609        if (!empty($data['scheduled_date']) && !empty($data['scheduled_time'])) {
610            return date(
611                'Y-m-d H:i:s',
612                strtotime($data['scheduled_date'] . ' ' . $data['scheduled_time'])
613            );
614        }
615
616        return null;
617    }
618
619    private function resolveRecipient($contact, ?string $channel): ?string
620    {
621        return match ($channel) {
622            'sms', 'whatsapp' => $contact->phone ?? null,
623            'email'           => $contact->email ?? null,
624            default           => $contact->phone ?? $contact->email ?? null,
625        };
626    }
627}