Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
50.00% covered (danger)
50.00%
64 / 128
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
SendTerminNotificationCommand
50.00% covered (danger)
50.00%
64 / 128
25.00% covered (danger)
25.00%
1 / 4
151.12
0.00% covered (danger)
0.00%
0 / 1
 getNotificationSubscriptionType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 autogenerateSubscriptions
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
11
 getTerminNotificationState
97.06% covered (success)
97.06%
33 / 34
0.00% covered (danger)
0.00%
0 / 1
7
 getNotifications
6.06% covered (danger)
6.06%
4 / 66
0.00% covered (danger)
0.00%
0 / 1
131.37
1<?php
2
3namespace Olz\Command\Notifications;
4
5use Doctrine\ORM\Query\ResultSetMapping;
6use Olz\Command\Notifications\Common\BaseSendNotificationsCommand;
7use Olz\Command\Notifications\Common\Notification;
8use Olz\Constants\NotificationDeliveryType;
9use Olz\Constants\NotificationType;
10use Olz\Entity\NotificationSubscription;
11use Olz\Entity\Roles\Role;
12use Olz\Entity\TelegramLink;
13use Olz\Entity\Throttling;
14use Olz\Entity\Users\User;
15use Olz\Utils\WithUtilsTrait;
16use Symfony\Component\Console\Attribute\AsCommand;
17
18#[AsCommand(name: 'olz:send-termin-notifications')]
19class SendTerminNotificationCommand extends BaseSendNotificationsCommand {
20    use WithUtilsTrait;
21
22    protected \DateTime $today;
23    protected \DateTime $yesterday;
24
25    public function getNotificationSubscriptionType(): NotificationType {
26        return NotificationType::TERMIN_NOTIFICATION;
27    }
28
29    public function autogenerateSubscriptions(): void {
30        $this->log()->info("Generating termin notification subscriptions...");
31        $termin_notifications_state = $this->getTerminNotificationState();
32
33        $now_datetime = new \DateTime($this->dateUtils()->getIsoNow());
34        $notification_subscription_repo = $this->entityManager()->getRepository(NotificationSubscription::class);
35        $user_repo = $this->entityManager()->getRepository(User::class);
36        foreach ($termin_notifications_state as $user_id => $user_state) {
37            foreach ($user_state as $delivery_type => $state) {
38                $subscription_id = $state['subscription_id'] ?? false;
39                $needs_subscription = $state['needs_subscription'] ?? false;
40                $user = $user_repo->findOneBy(['id' => $user_id]);
41                if (!$user) {
42                    $this->log()->warning("No user (ID:{$user_id}) for termin notification");
43                }
44                if ($needs_subscription && !$subscription_id && $user) {
45                    $this->log()->info("Generating termin notification subscription for '{$user}' via '{$delivery_type}'...");
46                    $subscription = new NotificationSubscription();
47                    $subscription->setUser($user);
48                    $subscription->setDeliveryType(NotificationDeliveryType::from($delivery_type));
49                    $subscription->setNotificationType(NotificationType::TERMIN_NOTIFICATION);
50                    $subscription->setNotificationTypeArgs(json_encode(['cancelled' => false]) ?: '{}');
51                    $subscription->setCreatedAt($now_datetime);
52                    $this->entityManager()->persist($subscription);
53                }
54                if ($subscription_id && !$needs_subscription) {
55                    $this->log()->info("Removing termin notification subscription ({$subscription_id}) for '{$user}' via '{$delivery_type}'...");
56                    $subscription = $notification_subscription_repo->findOneBy(['id' => $subscription_id]);
57                    if ($subscription) {
58                        $this->entityManager()->remove($subscription);
59                    }
60                }
61            }
62        }
63        $this->entityManager()->flush();
64    }
65
66    /**
67     * @return array<int, array<NotificationDeliveryType, array{
68     *   subscription_id?: int,
69     *   needs_subscription?: bool,
70     * }>> */
71    protected function getTerminNotificationState(): array {
72        $termin_notifications_state = [];
73
74        // Find users with existing termin notification subscriptions.
75        $notification_subscription_repo = $this->entityManager()->getRepository(NotificationSubscription::class);
76        $termin_notification_subscriptions = $notification_subscription_repo->findBy([
77            'notification_type' => NotificationType::TERMIN_NOTIFICATION,
78        ]);
79        foreach ($termin_notification_subscriptions as $subscription) {
80            $user_id = $subscription->getUser()->getId() ?: 0;
81            $delivery_type = $subscription->getDeliveryType()->value;
82            $user_state = $termin_notifications_state[$user_id][$delivery_type] ?? [];
83            $subscription_id = $subscription->getId();
84            $this->generalUtils()->checkNotNull($subscription_id, "No subscription ID");
85            $user_state['subscription_id'] = $subscription_id;
86            $termin_notifications_state[$user_id][$delivery_type] = $user_state;
87        }
88
89        // Find users who should have termin notification subscriptions.
90        $user_repo = $this->entityManager()->getRepository(User::class);
91        $users_with_email = $user_repo->getUsersWithLogin();
92        foreach ($users_with_email as $user_with_email) {
93            $user_id = $user_with_email->getId();
94            if (!$user_id) {
95                continue;
96            }
97            $delivery_type = NotificationDeliveryType::EMAIL->value;
98            $user_state = $termin_notifications_state[$user_id][$delivery_type] ?? [];
99            $user_state['needs_subscription'] = true;
100            $termin_notifications_state[$user_id][$delivery_type] = $user_state;
101        }
102        $telegram_link_repo = $this->entityManager()->getRepository(TelegramLink::class);
103        $telegram_links = $telegram_link_repo->getActivatedTelegramLinks();
104        foreach ($telegram_links as $telegram_link) {
105            $user_id = $telegram_link->getUser()?->getId();
106            if (!$user_id) {
107                continue;
108            }
109            $delivery_type = NotificationDeliveryType::TELEGRAM->value;
110            $user_state = $termin_notifications_state[$user_id][$delivery_type] ?? [];
111            $user_state['needs_subscription'] = true;
112            $termin_notifications_state[$user_id][$delivery_type] = $user_state;
113        }
114
115        return $termin_notifications_state;
116    }
117
118    // ---
119
120    public function getNotifications(array $args): array {
121        if ($args['cancelled'] ?? false) {
122            return [];
123        }
124
125        $ident = 'send-termin-notifications';
126        $throttling_repo = $this->entityManager()->getRepository(Throttling::class);
127        $last_occurrence = $throttling_repo->getLastOccurrenceOf($ident);
128        $now = new \DateTime($this->dateUtils()->getIsoNow());
129
130        // TODO: Add volunteers and participants
131        $sql = <<<'ZZZZZZZZZZ'
132            WITH
133                computed AS (
134                    SELECT
135                        ADDTIME(
136                            ADDDATE(t.start_date, INTERVAL -tn.fires_earlier_seconds SECOND),
137                            COALESCE(t.start_time, '12:00:00')
138                        ) AS fires_at,
139                        tn.title AS notification_title,
140                        tn.content AS notification_content,
141                        tn.recipient_user_id AS recipient_user_id,
142                        tn.recipient_role_id AS recipient_role_id,
143                        tn.recipient_termin_owner_user AS recipient_termin_owner_user,
144                        tn.recipient_termin_owner_role AS recipient_termin_owner_role,
145                        tn.recipient_termin_organizer AS recipient_termin_organizer,
146                        tn.recipient_termin_volunteers AS recipient_termin_volunteers,
147                        tn.recipient_termin_participants AS recipient_termin_participants,
148                        t.title AS termin_title,
149                        t.start_date AS termin_date,
150                        t.id AS termin_id,
151                        t.owner_user_id AS termin_owner_user_id,
152                        t.owner_role_id AS termin_owner_role_id,
153                        t.organizer_user_id AS termin_organizer_user_id
154                    FROM
155                        termin_notifications tn
156                        JOIN termine t ON (tn.termin_id = t.id)
157                )
158            SELECT *
159            FROM computed
160            WHERE fires_at < :now AND fires_at >= :last_occurrence
161            ZZZZZZZZZZ;
162
163        $rsm = new ResultSetMapping();
164        $rsm->addScalarResult('fires_at', 'firesAt');
165        $rsm->addScalarResult('notification_title', 'notificationTitle');
166        $rsm->addScalarResult('notification_content', 'notificationContent');
167        $rsm->addScalarResult('recipient_user_id', 'recipientUserId');
168        $rsm->addScalarResult('recipient_role_id', 'recipientRoleId');
169        $rsm->addScalarResult('recipient_termin_owner_user', 'recipientTerminOwnerUser');
170        $rsm->addScalarResult('recipient_termin_owner_role', 'recipientTerminOwnerRole');
171        $rsm->addScalarResult('recipient_termin_organizer', 'recipientTerminOrganizer');
172        $rsm->addScalarResult('recipient_termin_volunteers', 'recipientTerminVolunteers');
173        $rsm->addScalarResult('recipient_termin_participants', 'recipientTerminParticipants');
174        $rsm->addScalarResult('termin_title', 'terminTitle');
175        $rsm->addScalarResult('termin_date', 'terminDate');
176        $rsm->addScalarResult('termin_id', 'terminId');
177        $rsm->addScalarResult('termin_owner_user_id', 'terminOwnerUserId');
178        $rsm->addScalarResult('termin_owner_role_id', 'terminOwnerRoleId');
179        $rsm->addScalarResult('termin_organizer_user_id', 'terminOrganizerUserId');
180
181        $role_repo = $this->entityManager()->getRepository(Role::class);
182        $query = $this->entityManager()->createNativeQuery($sql, $rsm);
183        $query->setParameter('now', $now, 'datetime');
184        $query->setParameter('last_occurrence', $last_occurrence, 'datetime');
185        $result = $query->execute();
186
187        $base_href = $this->envUtils()->getBaseHref();
188        $code_href = $this->envUtils()->getCodeHref();
189        $termine_url = "{$base_href}{$code_href}termine";
190
191        $notifications = [];
192        foreach ($result as $row) {
193            $date = $this->dateUtils()->compactDate($row['terminDate']);
194            $title = "Termin-Erinnerung: {$row['notificationTitle']}";
195            $text = <<<ZZZZZZZZZZ
196                Hallo %%userFirstName%%,
197
198                Dies ist eine Termin-Erinnerung für den Termin [{$row['terminTitle']}]({$termine_url}/{$row['terminId']}) vom {$date}:
199
200                {$row['notificationContent']}
201
202                ZZZZZZZZZZ;
203            $recipient_user_ids = [];
204            if ($row['recipientUserId'] ?? false) {
205                $recipient_user_ids[] = $row['recipientUserId'];
206            }
207            if ($row['recipientRoleId'] ?? false) {
208                $role = $role_repo->findOneBy(['id' => $row['recipientRoleId']]);
209                foreach (($role?->getUsers() ?? []) as $user) {
210                    $recipient_user_ids[] = $user->getId();
211                }
212            }
213            if ($row['recipientTerminOwnerUser'] ?? false) {
214                $recipient_user_ids[] = $row['terminOwnerUserId'];
215            }
216            if ($row['recipientTerminOwnerRole'] ?? false) {
217                $role = $role_repo->findOneBy(['id' => $row['terminOwnerRoleId']]);
218                foreach (($role?->getUsers() ?? []) as $user) {
219                    $recipient_user_ids[] = $user->getId();
220                }
221            }
222            if ($row['recipientTerminOrganizer'] ?? false) {
223                $recipient_user_ids[] = $row['terminOrganizerUserId'];
224            }
225            if ($row['recipientTerminVolunteers'] ?? false) {
226                // TODO
227                $this->log()->notice("Termin notification to recipientTerminVolunteers: not implemented");
228            }
229            if ($row['recipientTerminParticipants'] ?? false) {
230                // TODO
231                $this->log()->notice("Termin notification to recipientTerminParticipants: not implemented");
232            }
233            $notifications[] = new Notification($title, $text, [
234                'notification_type' => NotificationType::TERMIN_NOTIFICATION,
235                'recipient_user_ids' => $recipient_user_ids,
236            ]);
237        }
238
239        return $notifications;
240    }
241}