Passed
Push — dependabot/npm_and_yarn/cross-... ( a16049...ad80c0 )
by
unknown
16:45 queued 07:24
created

LpProgressReminderCommand   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 272
Duplicated Lines 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
eloc 137
c 5
b 1
f 0
dl 0
loc 272
rs 9.44
wmc 37

9 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 9 1
C execute() 0 57 13
A __construct() 0 13 1
A logReminderSent() 0 11 3
A processCourseUsers() 0 34 5
B sendReminderIfNeeded() 0 31 7
A getNbReminder() 0 11 1
A isTimeToRemindUser() 0 13 3
A sendLpReminder() 0 43 3
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Command;
8
9
use Chamilo\CoreBundle\Repository\CourseRelUserRepository;
10
use Chamilo\CoreBundle\Repository\ExtraFieldValuesRepository;
11
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
12
use Chamilo\CoreBundle\Repository\Node\UserRepository;
13
use Chamilo\CoreBundle\Repository\SessionRelCourseRelUserRepository;
14
use Chamilo\CoreBundle\Repository\TrackEDefaultRepository;
15
use Chamilo\CoreBundle\ServiceHelper\MessageHelper;
16
use DateTime;
17
use DateTimeZone;
18
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
19
use Symfony\Contracts\Translation\TranslatorInterface;
20
use Twig\Environment;
21
use Symfony\Component\Console\Command\Command;
22
use Symfony\Component\Console\Input\InputInterface;
23
use Symfony\Component\Console\Input\InputOption;
24
use Symfony\Component\Console\Output\OutputInterface;
25
26
class LpProgressReminderCommand extends Command
27
{
28
    protected static $defaultName = 'app:lp-progress-reminder';
29
30
    private const NUMBER_OF_DAYS_TO_RESEND_NOTIFICATION = 3;
31
32
    public function __construct(
33
        private readonly CourseRepository $courseRepository,
34
        private readonly CourseRelUserRepository $courseRelUserRepository,
35
        private readonly SessionRelCourseRelUserRepository $sessionRelCourseRelUserRepository,
36
        private readonly ExtraFieldValuesRepository $extraFieldValuesRepository,
37
        private readonly TrackEDefaultRepository $trackEDefaultRepository,
38
        private readonly UserRepository $userRepository,
39
        private readonly Environment $twig,
40
        private readonly TranslatorInterface $translator,
41
        private readonly MessageHelper $messageHelper,
42
        private readonly UrlGeneratorInterface $urlGenerator
43
    ) {
44
        parent::__construct();
45
    }
46
47
48
    protected function configure()
49
    {
50
        $this
51
            ->setDescription('Send LP progress reminders to users based on "number_of_days_for_completion".')
52
            ->addOption(
53
                'debug',
54
                null,
55
                InputOption::VALUE_NONE,
56
                'If set, will output detailed debug information'
57
            );
58
    }
59
60
    protected function execute(InputInterface $input, OutputInterface $output): int
61
    {
62
        $debugMode = $input->getOption('debug');
63
        $output->writeln('Starting the LP progress reminder process...');
64
65
        // Retrieve LPs with completion days
66
        $lpItems = $this->extraFieldValuesRepository->getLpIdWithDaysForCompletion();
67
        if ($debugMode && !empty($lpItems)) {
68
            $output->writeln('LP Items retrieved: ' . print_r($lpItems, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($lpItems, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

68
            $output->writeln('LP Items retrieved: ' . /** @scrutinizer ignore-type */ print_r($lpItems, true));
Loading history...
69
        }
70
71
        if (empty($lpItems)) {
72
            $output->writeln('No learning paths with days for completion found.');
73
            return Command::SUCCESS;
74
        }
75
76
        $lpMap = [];
77
        foreach ($lpItems as $lpItem) {
78
            $lpMap[$lpItem['lp_id']] = $lpItem['ndays'];
79
        }
80
        $lpIds = array_keys($lpMap);
81
82
        // Retrieve all courses from the CourseRepository
83
        $courses = $this->courseRepository->findAll();
84
        if ($debugMode && !empty($courses)) {
85
            $output->writeln('Courses retrieved: ' . count($courses));
86
        }
87
88
        foreach ($courses as $course) {
89
            $courseId = $course->getId();
90
91
            // Retrieve users for the course (without session)
92
            $courseUsers = $this->courseRelUserRepository->getCourseUsers($courseId, $lpIds);
93
            // Retrieve users for the course session
94
            $sessionCourseUsers = $this->sessionRelCourseRelUserRepository->getSessionCourseUsers($courseId, $lpIds);
95
96
            if ($debugMode && (!empty($courseUsers) || !empty($sessionCourseUsers))) {
97
                $output->writeln('Processing course ID: ' . $courseId);
98
                if (!empty($courseUsers)) {
99
                    $output->writeln('Course users retrieved: ' . count($courseUsers));
100
                    //$output->writeln('Course retrieved: ' . print_r($courseUsers, true));
101
                }
102
                if (!empty($sessionCourseUsers)) {
103
                    $output->writeln('Session users retrieved: ' . count($sessionCourseUsers));
104
                    //$output->writeln('Session retrieved: ' . print_r($sessionCourseUsers, true));
105
                }
106
            }
107
108
            // Process users from the main course (sessionId = 0 or null)
109
            $this->processCourseUsers($courseUsers, $lpMap, $courseId, $debugMode);
110
111
            // Process users from the course session (sessionId > 0)
112
            $this->processCourseUsers($sessionCourseUsers, $lpMap, $courseId, $debugMode, true);
113
        }
114
115
        $output->writeln('LP progress reminder process finished.');
116
        return Command::SUCCESS;
117
    }
118
119
    /**
120
     * Processes users from a course or session to check if a reminder needs to be sent.
121
     */
122
    private function processCourseUsers(array $users, array $lpItems, int $courseId, bool $debugMode = false, bool $checkSession = false): void
123
    {
124
        foreach ($users as $user) {
125
            $userId = $user['userId'];
126
            $courseTitle = $user['courseTitle'];
127
            $lpId = $user['lpId'];
128
            $progress = (int) $user['progress'];
129
130
            $sessionId = $checkSession ? ($user['sessionId'] ?? 0) : 0;
131
132
            if ($lpId === null) {
133
                foreach ($lpItems as $lpId => $nbDaysForLpCompletion) {
134
                    $this->sendReminderIfNeeded(
135
                        $userId,
136
                        $courseTitle,
137
                        $courseId,
138
                        $sessionId,
139
                        (int) $nbDaysForLpCompletion,
140
                        $debugMode,
141
                        $lpId,
142
                        $progress
143
                    );
144
                }
145
            } else {
146
                $nbDaysForLpCompletion = (int) ($lpItems[$lpId] ?? self::NUMBER_OF_DAYS_TO_RESEND_NOTIFICATION);
147
                $this->sendReminderIfNeeded(
148
                    $userId,
149
                    $courseTitle,
150
                    $courseId,
151
                    $sessionId,
152
                    $nbDaysForLpCompletion,
153
                    $debugMode,
154
                    $lpId,
155
                    $progress
156
                );
157
            }
158
        }
159
    }
160
161
    /**
162
     * Sends a progress reminder to a user if the conditions for reminder timing are met,
163
     * based on their registration date and learning path completion criteria.
164
     */
165
    private function sendReminderIfNeeded(
166
        int $userId,
167
        string $courseTitle,
168
        int $courseId,
169
        int $sessionId,
170
        int $nbDaysForLpCompletion,
171
        bool $debugMode,
172
        ?int $lpId,
173
        int $progress
174
    ): void {
175
        $registrationDate = $this->trackEDefaultRepository->getUserCourseRegistrationAt($courseId, $userId, $sessionId);
176
        if (!$registrationDate) {
0 ignored issues
show
introduced by
$registrationDate is of type DateTime, thus it always evaluated to true.
Loading history...
177
            if ($debugMode) {
178
                echo "No registration date found for user $userId in course $courseId (session ID: $sessionId).\n";
179
            }
180
            return;
181
        }
182
183
        if ($debugMode) {
184
            $sessionInfo = $sessionId > 0 ? "in session ID $sessionId" : "without a session";
185
            echo "Registration date: {$registrationDate->format('Y-m-d H:i:s')}, Days for completion: $nbDaysForLpCompletion, LP ID: {$lpId}, $sessionInfo\n";
186
        }
187
188
        if ($this->isTimeToRemindUser($registrationDate, $nbDaysForLpCompletion)) {
189
            $nbRemind = $this->getNbReminder($registrationDate, $nbDaysForLpCompletion);
190
191
            if ($debugMode) {
192
                echo "Sending reminder to user $userId for course $courseTitle (LP ID: {$lpId})\n";
193
            }
194
            $this->logReminderSent($userId, $courseTitle, $nbRemind, $debugMode, $lpId, $sessionId);
195
            $this->sendLpReminder($userId, $courseTitle, $progress, $registrationDate, $nbRemind);
196
        }
197
    }
198
199
    /**
200
     * Logs the reminder details if debug mode is enabled.
201
     */
202
    private function logReminderSent(int $userId, string $courseTitle, int $nbRemind, bool $debugMode, int $lpId, int $sessionId = 0): void
203
    {
204
        if ($debugMode) {
205
            $sessionInfo = $sessionId > 0 ? sprintf("in session ID %d", $sessionId) : "without a session";
206
            echo sprintf(
207
                "Reminder number %d sent to user ID %d for the course %s (LP ID: %d) %s.\n",
208
                $nbRemind,
209
                $userId,
210
                $courseTitle,
211
                $lpId,
212
                $sessionInfo
213
            );
214
        }
215
    }
216
217
    /**
218
     * Calculates the number of reminders to be sent based on registration date and days for completion.
219
     */
220
    private function getNbReminder(DateTime $registrationDate, int $nbDaysForLpCompletion): int
221
    {
222
        $reminderStartDate = (clone $registrationDate)->modify("+$nbDaysForLpCompletion days");
223
        $currentDate = new DateTime('now', new DateTimeZone('UTC'));
224
        $reminderStartDate->setTime(0, 0, 0);
225
        $currentDate->setTime(0, 0, 0);
226
227
        $interval = $reminderStartDate->diff($currentDate);
228
        $diffDays = (int) $interval->format('%a');
229
230
        return (int) floor($diffDays / self::NUMBER_OF_DAYS_TO_RESEND_NOTIFICATION) + 1;
231
    }
232
233
    /**
234
     * Checks if it is time to remind the user based on their registration date and LP completion days.
235
     */
236
    private function isTimeToRemindUser(DateTime $registrationDate, int $nbDaysForLpCompletion): bool
237
    {
238
        $reminderStartDate = (clone $registrationDate)->modify("+$nbDaysForLpCompletion days");
239
        $reminderStartDate->setTime(0, 0, 0);
240
241
        $currentDate = new DateTime('now', new DateTimeZone('UTC'));
242
        $currentDate->setTime(0, 0, 0);
243
244
        $interval = $reminderStartDate->diff($currentDate);
245
        $diffDays = (int) $interval->format('%a');
246
247
        return ($diffDays >= self::NUMBER_OF_DAYS_TO_RESEND_NOTIFICATION &&
248
            $diffDays % self::NUMBER_OF_DAYS_TO_RESEND_NOTIFICATION === 0) || $diffDays === 0;
249
    }
250
251
252
    /**
253
     * Sends a reminder email to the user regarding their LP progress.
254
     */
255
    private function sendLpReminder(int $toUserId, string $courseName, int $lpProgress, DateTime $registrationDate, int $nbRemind): bool
256
    {
257
        $user = $this->userRepository->find($toUserId);
258
        if (!$user) {
259
            throw new \Exception("User not found");
260
        }
261
262
        $platformUrl = $this->urlGenerator->generate('index', [], UrlGeneratorInterface::ABSOLUTE_URL);
263
        $recoverPasswordUrl = $platformUrl.'/main/auth/lostPassword.php';
264
265
        $trainingCenterName = 'Your Training Center';
266
        $trainers = 'Trainer Name';
267
268
        $hello = $this->translator->trans("Hello %s");
269
        $youAreRegCourse = $this->translator->trans("You are registered in the training %s since the %s");
270
        $thisMessageIsAbout = $this->translator->trans("You are receiving this message because you have completed a learning path with a %s progress of your training.<br/>Your progress must be 100 to consider that your training was carried out.<br/>If you have the slightest problem, you should contact with your trainer.");
271
        $stepsToRemind = $this->translator->trans("As a reminder, to access the training platform:<br/>1. Connect to the platform at the address: %s <br/>2. Then enter: <br/>Your username: %s <br/>Your password: This was emailed to you.<br/>if you forgot it and can't find it, you can retrieve it by going to %s <br/><br/>Thank you for doing what is necessary.");
272
        $lpRemindFooter = $this->translator->trans("The training center<p>%s</p>Trainers:<br/>%s");
273
274
        $hello = sprintf($hello, $user->getFullName());
275
        $youAreRegCourse = sprintf($youAreRegCourse, $courseName, $registrationDate->format('Y-m-d'));
276
        $thisMessageIsAbout = sprintf($thisMessageIsAbout, $lpProgress);
277
        $stepsToRemind = sprintf($stepsToRemind, $platformUrl, $user->getUsername(), $recoverPasswordUrl);
278
        $lpRemindFooter = sprintf($lpRemindFooter, $trainingCenterName, $trainers);
279
280
        $messageContent = $this->twig->render('@ChamiloCore/Mailer/Legacy/lp_progress_reminder_body.html.twig', [
281
            'HelloX' => $hello,
282
            'YouAreRegCourseXFromDateX' => $youAreRegCourse,
283
            'ThisMessageIsAboutX' => $thisMessageIsAbout,
284
            'StepsToRemindX' => $stepsToRemind,
285
            'LpRemindFooterX' => $lpRemindFooter,
286
        ]);
287
288
        try {
289
            $this->messageHelper->sendMessageSimple(
290
                $toUserId,
291
                sprintf("Reminder number %d for the course %s", $nbRemind, $courseName),
292
                $messageContent
293
            );
294
295
            return true;
296
        } catch (\Exception $e) {
297
            throw new \Exception('Error sending reminder: ' . $e->getMessage());
298
        }
299
    }
300
}
301