Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.29% covered (warning)
68.29%
28 / 41
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
NotificationAttemptMapper
68.29% covered (warning)
68.29%
28 / 41
66.67% covered (warning)
66.67%
4 / 6
10.04
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
 begin
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 finish
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 findForDelivery
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 finishInterrupted
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 deleteOlderThan
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
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\Db;
10
11use OCA\Libresign\Enum\NotificationDeliveryState;
12use OCP\AppFramework\Db\QBMapper;
13use OCP\DB\QueryBuilder\IQueryBuilder;
14use OCP\IDBConnection;
15
16/** @template-extends QBMapper<NotificationAttempt> */
17class NotificationAttemptMapper extends QBMapper {
18    public function __construct(IDBConnection $db) {
19        parent::__construct($db, 'libresign_notification_attempt', NotificationAttempt::class);
20    }
21
22    public function begin(int $deliveryId): NotificationAttempt {
23        $attempt = new NotificationAttempt();
24        $attempt->setDeliveryId($deliveryId);
25        $attempt->setState(NotificationDeliveryState::SENDING->value);
26        $attempt->setStartedAt(new \DateTime('now', new \DateTimeZone('UTC')));
27        return $this->insert($attempt);
28    }
29
30    public function finish(int $attemptId, NotificationDeliveryState $state, ?string $reasonCode): void {
31        $qb = $this->db->getQueryBuilder();
32        $updated = $qb->update($this->getTableName())
33            ->set('state', $qb->createNamedParameter($state->value))
34            ->set('reason_code', $qb->createNamedParameter($reasonCode))
35            ->set('finished_at', $qb->createNamedParameter(new \DateTime('now', new \DateTimeZone('UTC')), IQueryBuilder::PARAM_DATETIME_MUTABLE))
36            ->where($qb->expr()->eq('id', $qb->createNamedParameter($attemptId, IQueryBuilder::PARAM_INT)))
37            ->andWhere($qb->expr()->eq('state', $qb->createNamedParameter(NotificationDeliveryState::SENDING->value)))
38            ->executeStatement();
39        if ($updated !== 1) {
40            throw new \LogicException('Notification attempt state changed during dispatch');
41        }
42    }
43
44    /** @return list<NotificationAttempt> */
45    public function findForDelivery(int $deliveryId): array {
46        $qb = $this->db->getQueryBuilder();
47        $qb->select('*')->from($this->getTableName())
48            ->where($qb->expr()->eq('delivery_id', $qb->createNamedParameter($deliveryId, IQueryBuilder::PARAM_INT)))
49            ->orderBy('started_at', 'ASC');
50        return $this->findEntities($qb);
51    }
52
53    /** Close interrupted submissions without marking them as safe to retry. */
54    public function finishInterrupted(int $deliveryId): int {
55        $qb = $this->db->getQueryBuilder();
56        return $qb->update($this->getTableName())
57            ->set('state', $qb->createNamedParameter(NotificationDeliveryState::UNKNOWN->value))
58            ->set('reason_code', $qb->createNamedParameter('submission_interrupted'))
59            ->set('finished_at', $qb->createNamedParameter(new \DateTime('now', new \DateTimeZone('UTC')), IQueryBuilder::PARAM_DATETIME_MUTABLE))
60            ->where($qb->expr()->eq('delivery_id', $qb->createNamedParameter($deliveryId, IQueryBuilder::PARAM_INT)))
61            ->andWhere($qb->expr()->eq('state', $qb->createNamedParameter(NotificationDeliveryState::SENDING->value)))
62            ->executeStatement();
63    }
64
65    public function deleteOlderThan(\DateTime $before, int $limit = 500): int {
66        $qb = $this->db->getQueryBuilder();
67        $rows = $qb->select('id')->from($this->getTableName())
68            ->where($qb->expr()->lt('finished_at', $qb->createNamedParameter($before, IQueryBuilder::PARAM_DATETIME_MUTABLE)))
69            ->setMaxResults($limit)
70            ->executeQuery()->fetchAllAssociative();
71        if ($rows === []) {
72            return 0;
73        }
74        $ids = array_map(static fn (array $row): int => (int)$row['id'], $rows);
75        $delete = $this->db->getQueryBuilder();
76        return $delete->delete($this->getTableName())
77            ->where($delete->expr()->in('id', $delete->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)))
78            ->executeStatement();
79    }
80}