Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
NotificationDispatchService
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 6
380
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 dispatch
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 process
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
56
 normalizePayload
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 renderMessage
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 fail
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace App\Modules\NotificationModule\Services;
4
5use App\Modules\NotificationModule\Models\NotificationQueueModel;
6use App\Modules\NotificationModule\Models\NotificationAuditModel;
7use App\Modules\NotificationModule\Models\NotificationMetricModel;
8
9class NotificationDispatchService
10{
11    protected NotificationQueueModel $queueModel;
12    protected NotificationAuditModel $auditModel;
13    protected NotificationMetricModel $metricModel;
14    protected SmsRateLimiterService $rateLimiter;
15
16    public function __construct()
17    {
18        $this->queueModel  = new NotificationQueueModel();
19        $this->auditModel  = new NotificationAuditModel();
20        $this->metricModel = new NotificationMetricModel();
21        $this->rateLimiter = service('smsRateLimiterService');
22    }
23
24    public function dispatch(object $notification): bool
25    {
26        try {
27            return $this->process($notification);
28        } catch (\Throwable $e) {
29            log_message('error', '[DISPATCH] ' . $e->getMessage());
30            throw $e; // ← remonte au contrôleur
31        }
32    }
33
34 private function process(object $notification): bool
35{
36    $start         = microtime(true);
37    $templateService = service('messageTemplateService');
38    $channelRouter   = service('channelRouterService');
39
40    $payload = $this->normalizePayload($notification->payload);
41
42    log_message('debug', '[DISPATCH] payload brut type = ' . gettype($notification->payload));
43    log_message('debug', '[DISPATCH] payload brut = '      . print_r($notification->payload, true));
44    log_message('debug', '[DISPATCH] payload normalisé = ' . json_encode($payload));
45    log_message('debug', '[DISPATCH] provider = '          . ($notification->provider ?? 'NULL'));
46
47    if (empty($payload['to'])) {
48        $this->fail($notification, 'Destinataire manquant');
49        throw new \RuntimeException('Destinataire manquant dans le payload');
50    }
51
52    $message      = $this->renderMessage($payload, $templateService);
53    $providerCode = $notification->provider ?? null;
54
55    // ── RATE LIMIT ────────────────────────────────────────────────
56    $this->rateLimiter->sleepForThrottle();
57
58    if (!$this->rateLimiter->allow($providerCode ?? 'unknown')) {
59        $this->fail($notification, 'Rate limit atteint');
60        throw new \RuntimeException('Rate limit atteint');
61    }
62
63    // ── DISPATCH ──────────────────────────────────────────────────
64    try {
65        if ($providerCode) {
66            // ← Utilise le provider exact stocké en queue
67            $response = $channelRouter->routeWithProvider(
68                $notification->channel,
69                $payload,
70                $message,
71                $providerCode
72            );
73        } else {
74            // ← Fallback : résolution automatique par canal/pays
75            log_message('warning', '[DISPATCH] provider non défini — fallback résolution auto');
76            $response = $channelRouter->route(
77                $notification->channel,
78                $payload,
79                $message
80            );
81        }
82    } catch (\Throwable $routerEx) {
83        $this->fail($notification, $routerEx->getMessage());
84        throw $routerEx;
85    }
86
87    // ── VÉRIFIE RÉSULTAT ──────────────────────────────────────────
88    if ($response === false || $response === null) {
89        $this->fail($notification, 'Provider a retourné false');
90        throw new \RuntimeException(
91            'Provider retourné false — canal=' . $notification->channel
92            . ' to=' . ($payload['to'] ?? 'null')
93        );
94    }
95
96    // ── SUCCESS ───────────────────────────────────────────────────
97    $this->queueModel->update($notification->id, [
98        'status'        => 'sent',
99        'processed_at'  => date('Y-m-d H:i:s'),
100        'attempts'      => ((int) $notification->attempts) + 1,
101        'error_message' => null,
102    ]);
103
104    $this->auditModel->insert([
105        'notification_id' => $notification->id,
106        'provider'        => $notification->provider,
107        'request'         => json_encode($payload),
108        'response'        => json_encode($response),
109        'status'          => 'sent',
110        'latency_ms'      => (int)((microtime(true) - $start) * 1000),
111    ]);
112
113    $this->metricModel->increment(
114        $notification->channel,
115        $notification->provider,
116        true
117    );
118
119    return true;
120}
121
122    private function normalizePayload($payload): array
123    {
124        if (is_string($payload)) {
125            $payload = json_decode($payload, true);
126        }
127
128        if (is_object($payload)) {
129            $payload = json_decode(json_encode($payload), true);
130        }
131
132        return is_array($payload) ? $payload : [];
133    }
134
135    private function renderMessage(array $payload, $templateService): string
136    {
137        $message = $payload['message'] ?? '';
138
139        if (!empty($payload['contact']) && is_array($payload['contact'])) {
140            $message = $templateService->render($message, [
141                'name'    => $payload['contact']['name'] ?? '',
142                'email'   => $payload['contact']['email'] ?? '',
143                'phone'   => $payload['contact']['phone'] ?? '',
144                'company' => $payload['contact']['company'] ?? '',
145            ]);
146        }
147
148        return $message;
149    }
150
151    private function fail(object $notification, string $error): void
152    {
153        $attempts = ((int) $notification->attempts) + 1;
154
155        $this->queueModel->update($notification->id, [
156            'status'        => ($attempts >= 3) ? 'failed' : 'pending',
157            'attempts'      => $attempts,
158            'error_message' => $error,
159        ]);
160    }
161}