Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 119
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SchedulerController
0.00% covered (danger)
0.00%
0 / 119
0.00% covered (danger)
0.00%
0 / 6
462
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 index
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 generate
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
30
 processQueue
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
42
 status
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 isAuthorizedCron
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace App\Modules\SchedulerModule\Controllers;
4
5use App\Controllers\BaseController;
6use App\Modules\NotificationModule\Services\NotificationPipelineService;
7use App\Modules\NotificationModule\Services\NotificationDispatchService;
8use App\Modules\CustomerModule\Models\ContractModel;
9
10class SchedulerController extends BaseController
11{
12    protected NotificationPipelineService $pipeline;
13    protected NotificationDispatchService $dispatchService;
14    protected ContractModel $contractModel;
15
16    public function __construct()
17    {
18        $this->pipeline       = new NotificationPipelineService();
19        $this->dispatchService = new NotificationDispatchService();
20        $this->contractModel  = new ContractModel();
21    }
22
23    /**
24     * DASHBOARD MONITORING
25     */
26    public function index()
27    {
28        $queueModel = model(\App\Modules\NotificationModule\Models\NotificationQueueModel::class);
29
30        return view('App\Modules\SchedulerModule\Views\index', [
31            'title' => 'Scheduler Monitoring',
32
33            'stats' => [
34                'pending' => $queueModel->where('status', 'pending')->countAllResults(true),
35                'sent' => $queueModel->where('status', 'sent')->countAllResults(true),
36                'failed' => $queueModel->where('status', 'failed')->countAllResults(true),
37                'processing' => $queueModel->where('status', 'processing')->countAllResults(true),
38            ],
39
40            'lastQueue' => $queueModel
41                ->orderBy('id', 'DESC')
42                ->findAll(20),
43        ]);
44    }
45
46    /**
47     * CRON ENTRY POINT : génération des notifications
48     */
49    public function generate()
50    {
51        try {
52            if (!$this->isAuthorizedCron()) {
53                return $this->response
54                    ->setStatusCode(403)
55                    ->setJSON(['error' => 'unauthorized']);
56            }
57
58            $contracts = $this->contractModel
59                ->where('notification_enabled', 1)
60                ->findAll();
61
62            $processed = 0;
63            $queueModel = model(\App\Modules\NotificationModule\Models\NotificationQueueModel::class);
64
65            foreach ($contracts as $contract) {
66                try {
67                    log_message('debug', "=== DÉBUT processContract ID {$contract->id} ===");
68
69                    $this->pipeline->processContract($contract->id);
70
71                    log_message('debug', "=== FIN processContract ID {$contract->id} ===");
72                    $processed++;
73
74                } catch (\Throwable $e) {
75                    log_message('error', "Erreur processContract {$contract->id} : " . $e->getMessage() . "\n" . $e->getTraceAsString());
76                }
77            }
78
79            $totalInQueue = $queueModel->countAllResults();
80
81            return $this->response->setJSON([
82                'status'           => 'ok',
83                'contracts_found'  => count($contracts),
84                'processed'        => $processed,
85                'total_in_queue'   => $totalInQueue,
86                'message'          => 'Regardez les logs serveur'
87            ]);
88
89        } catch (\Throwable $e) {
90            log_message('error', 'Erreur generate() : ' . $e->getMessage());
91            return $this->response->setStatusCode(500)
92                ->setJSON(['error' => $e->getMessage()]);
93        }
94    }
95
96
97    /**
98     * CRON WORKER : traitement queue
99     */
100    public function processQueue()
101    {
102        if (!$this->isAuthorizedCron()) {
103            return $this->response
104                ->setStatusCode(403)
105                ->setJSON([
106                    'error'   => 'unauthorized',
107                    'message' => 'Token manquant ou invalide'
108                ]);
109        }
110
111        $queueModel = service('notificationQueueModel');
112        $db = db_connect();
113
114        try {
115            $db->transStart();
116
117            // 1. LOCK atomique des éléments à traiter
118            $db->query("
119                UPDATE notification_queue
120                SET status = 'processing'
121                WHERE status = 'pending'
122                AND scheduled_at <= NOW()
123                ORDER BY scheduled_at ASC, id ASC
124                LIMIT 20
125            ");
126
127            $db->transComplete();
128
129            if ($db->transStatus() === false) {
130                return $this->response->setJSON([
131                    'processed' => 0,
132                    'message'   => 'Transaction failed'
133                ]);
134            }
135
136            // 2. Récupération exclusive des items lockés
137            $queue = $queueModel
138                ->where('status', 'processing')
139                ->orderBy('id', 'ASC')
140                ->findAll(20);
141
142            $processed = 0;
143
144            foreach ($queue as $item) {
145                try {
146                    $this->dispatchService->retry($item);
147
148                    $queueModel->update($item->id, [
149                        'status' => 'sent',
150                        'attempts' => (int) $item->attempts + 1
151                    ]);
152
153                    $processed++;
154
155                } catch (\Throwable $e) {
156
157                    $queueModel->markAsFailed(
158                        $item->id,
159                        (int) $item->attempts + 1,
160                        $e->getMessage()
161                    );
162                }
163            }
164
165            return $this->response->setJSON([
166                'processed' => $processed
167            ]);
168
169        } catch (\Throwable $e) {
170
171            log_message('error', 'processQueue error: ' . $e->getMessage());
172
173            return $this->response->setStatusCode(500)
174                ->setJSON([
175                    'error' => $e->getMessage()
176                ]);
177        }
178    }
179
180    /**
181     * STATUS API (AJAX DASHBOARD)
182     */
183/**
184 * STATUS API (AJAX DASHBOARD)
185 */
186public function status()
187{
188    try {
189        $queueModel = model(\App\Modules\NotificationModule\Models\NotificationQueueModel::class);
190
191        return $this->response->setJSON([
192            'pending'    => (clone $queueModel)->where('status', 'pending')->countAllResults(),
193            'sent'       => (clone $queueModel)->where('status', 'sent')->countAllResults(),
194            'failed'     => (clone $queueModel)->where('status', 'failed')->countAllResults(),
195            'processing' => (clone $queueModel)->where('status', 'processing')->countAllResults(),
196        ]);
197
198    } catch (\Throwable $e) {
199        log_message('error', 'Erreur status() : ' . $e->getMessage());
200        return $this->response->setJSON([
201            'pending' => 0,
202            'sent' => 0,
203            'failed' => 0,
204            'processing' => 0
205        ]);
206    }
207}
208
209
210    /**
211     * CRON SECURITY - Version Debug
212     */
213    private function isAuthorizedCron(): bool
214    {
215        $envToken = getenv('CRON_TOKEN');
216        $queryToken = $this->request->getGet('token');
217        $headerToken = $this->request->getHeaderLine('X-CRON-TOKEN');
218
219        log_message('debug', '=== CRON TOKEN DEBUG ===');
220        log_message('debug', 'ENV Token length : ' . strlen($envToken ?? ''));
221        log_message('debug', 'Query Token present : ' . ($queryToken ? 'YES' : 'NO'));
222        log_message('debug', 'Header Token present : ' . ($headerToken ? 'YES' : 'NO'));
223
224        if (empty($envToken)) {
225            log_message('error', 'CRON_TOKEN est vide ou non chargé depuis .env');
226            return false;
227        }
228
229        if ($queryToken === $envToken || $headerToken === $envToken) {
230            log_message('debug', 'CRON Token → AUTORISÉ');
231            return true;
232        }
233
234        log_message('debug', 'CRON Token → REFUSÉ');
235        return false;
236    }
237}