Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.58% covered (warning)
81.58%
31 / 38
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
NotificationDeliveryProcessor
81.58% covered (warning)
81.58%
31 / 38
50.00% covered (danger)
50.00%
1 / 2
14.06
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 process
81.08% covered (warning)
81.08%
30 / 37
0.00% covered (danger)
0.00%
0 / 1
12.98
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service\Notification;
10
11use OCA\Libresign\Db\NotificationDeliveryMapper;
12use OCA\Libresign\Db\SignRequestMapper;
13use OCA\Libresign\Enum\NotificationDeliveryState;
14use OCA\Libresign\Enum\SignRequestStatus;
15use OCP\AppFramework\Db\DoesNotExistException;
16use Psr\Log\LoggerInterface;
17
18/**
19 * Process a durable notification. Only PENDING records and due safe retries are eligible.
20 * A claimed transport attempt is never retried after an uncertain outcome.
21 */
22final class NotificationDeliveryProcessor {
23    public function __construct(
24        private NotificationDeliveryMapper $deliveries,
25        private SignRequestMapper $signRequests,
26        private NotificationEnvelopeCodec $codec,
27        private NotificationTransportRegistry $transports,
28        private NotificationSubmissionService $submission,
29        private LoggerInterface $logger,
30    ) {
31    }
32
33    public function process(int $deliveryId): void {
34        try {
35            $delivery = $this->deliveries->findById($deliveryId);
36        } catch (DoesNotExistException) {
37            // An old queued job may remain after the retention job deleted its row.
38            return;
39        }
40        $isPending = $delivery->getState() === NotificationDeliveryState::PENDING->value;
41        $retryAt = $delivery->getNextAttemptAt();
42        $isDueRetry = $delivery->getState() === NotificationDeliveryState::TEMPORARY_FAILURE->value
43            && $retryAt !== null
44            && $retryAt <= new \DateTime('now', new \DateTimeZone('UTC'));
45        if (!$isPending && !$isDueRetry) {
46            return;
47        }
48        if ($delivery->getEventType() === 'sign_request') {
49            // Never resurrect invitations for removed, rejected or already-signed
50            // participants. Cancellation notices use their stored snapshot instead.
51            try {
52                $request = $this->signRequests->getByIdUncached($delivery->getSignRequestId());
53                $actionable = $request->getSigned() === null && in_array(
54                    $request->getStatusEnum(),
55                    [SignRequestStatus::ABLE_TO_SIGN, SignRequestStatus::OBSERVING],
56                    true,
57                );
58            } catch (DoesNotExistException) {
59                $actionable = false;
60            }
61            if (!$actionable) {
62                $this->deliveries->cancelPendingInvitations($delivery->getSignRequestId());
63                return;
64            }
65        }
66        try {
67            $message = $this->codec->decode($delivery);
68            $transport = $this->transports->resolve($message->channel);
69        } catch (\Throwable $error) {
70            $this->deliveries->failPending($deliveryId);
71            $this->logger->warning('Unable to prepare queued notification', [
72                'deliveryId' => $deliveryId,
73                'exceptionType' => get_debug_type($error),
74            ]);
75            return;
76        }
77        try {
78            $this->submission->submit($delivery, $message, $transport);
79        } catch (\Exception $error) {
80            // submit() has already recorded the definitive or unknown outcome.
81            // Exceptions must not interfere with processing another delivery.
82            $this->logger->warning('Queued notification transport failed', [
83                'deliveryId' => $deliveryId,
84                'exceptionType' => get_debug_type($error),
85            ]);
86        }
87    }
88}