• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LibreSign / libresign / 20104047174

10 Dec 2025 03:32PM UTC coverage: 43.803%. First build
20104047174

Pull #6069

github

web-flow
Merge 5caf995ab into a49dfb931
Pull Request #6069: feat: sequential signing

46 of 146 new or added lines in 10 files covered. (31.51%)

5733 of 13088 relevant lines covered (43.8%)

5.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

20.63
/lib/Service/SequentialSigningService.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8

9
namespace OCA\Libresign\Service;
10

11
use OCA\Libresign\AppInfo\Application;
12
use OCA\Libresign\Db\SignRequestMapper;
13
use OCA\Libresign\Db\SignRequestStatus;
14
use OCP\IAppConfig;
15

16
class SequentialSigningService {
17
        private int $currentOrder = 1;
18

19
        public function __construct(
20
                private IAppConfig $appConfig,
21
                private SignRequestMapper $signRequestMapper,
22
                private IdentifyMethodService $identifyMethodService,
23
        ) {
24
        }
47✔
25

26
        /**
27
         * Check if ordered numeric flow is enabled
28
         */
29
        public function isOrderedNumericFlow(): bool {
30
                return $this->getSignatureFlow() === SignatureFlow::ORDERED_NUMERIC;
13✔
31
        }
32

33
        /**
34
         * Reset the internal order counter
35
         */
36
        public function resetOrderCounter(): void {
37
                $this->currentOrder = 1;
13✔
38
        }
39

40
        /**
41
         * Determine signing order based on flow configuration
42
         * Manages internal counter automatically
43
         *
44
         * @param int|null $userProvidedOrder Order explicitly set by user
45
         * @return int The order to use
46
         */
47
        public function determineSigningOrder(?int $userProvidedOrder): int {
48
                if (!$this->isOrderedNumericFlow()) {
13✔
49
                        return 1;
13✔
50
                }
51

NEW
52
                if ($userProvidedOrder !== null) {
×
NEW
53
                        if ($userProvidedOrder > $this->currentOrder) {
×
NEW
54
                                $this->currentOrder = $userProvidedOrder;
×
55
                        }
NEW
56
                        return $userProvidedOrder;
×
57
                }
58

NEW
59
                return $this->currentOrder++;
×
60
        }
61

62
        /**
63
         * Release next order of signers after current order is completed
64
         * Called when a signature is saved
65
         *
66
         * @param int $fileId
67
         * @param int $completedOrder The order that was just completed
68
         */
69
        public function releaseNextOrder(int $fileId, int $completedOrder): void {
NEW
70
                if (!$this->isOrderedNumericFlow()) {
×
NEW
71
                        return;
×
72
                }
73

NEW
74
                $allSignRequests = $this->signRequestMapper->getByFileId($fileId);
×
75

NEW
76
                if (!$this->isOrderFullyCompleted($allSignRequests, $completedOrder)) {
×
NEW
77
                        return;
×
78
                }
79

NEW
80
                $nextOrder = $this->findNextOrder($allSignRequests, $completedOrder);
×
NEW
81
                if ($nextOrder === null) {
×
NEW
82
                        return;
×
83
                }
84

NEW
85
                $this->activateSignersForOrder($allSignRequests, $nextOrder);
×
86
        }
87

88
        /**
89
         * Reorder and activate signers after a SignRequest deletion
90
         * This ensures no gaps in the signing sequence
91
         *
92
         * @param int $fileId The file ID
93
         * @param int $deletedOrder The order that was deleted
94
         */
95
        public function reorderAfterDeletion(int $fileId, int $deletedOrder): void {
96
                if (!$this->isOrderedNumericFlow()) {
2✔
97
                        return;
2✔
98
                }
99

NEW
100
                $allSignRequests = $this->signRequestMapper->getByFileId($fileId);
×
101

NEW
102
                $hasSignersAtDeletedOrder = !empty(array_filter(
×
NEW
103
                        $allSignRequests,
×
NEW
104
                        fn ($sr) => $sr->getSigningOrder() === $deletedOrder
×
NEW
105
                ));
×
106

NEW
107
                if (!$hasSignersAtDeletedOrder) {
×
NEW
108
                        $previousOrder = $deletedOrder - 1;
×
NEW
109
                        if ($previousOrder > 0 && $this->isOrderFullyCompleted($allSignRequests, $previousOrder)) {
×
NEW
110
                                $nextOrder = $this->findNextOrder($allSignRequests, $deletedOrder);
×
NEW
111
                                if ($nextOrder !== null) {
×
NEW
112
                                        $this->activateSignersForOrder($allSignRequests, $nextOrder);
×
113
                                }
114
                        }
115
                }
116
        }
117

118
        private function isOrderFullyCompleted(array $signRequests, int $order): bool {
NEW
119
                $pendingSigners = array_filter(
×
NEW
120
                        $signRequests,
×
NEW
121
                        fn ($sr) => $sr->getSigningOrder() === $order
×
NEW
122
                                && $sr->getStatusEnum() !== SignRequestStatus::SIGNED
×
NEW
123
                );
×
124

NEW
125
                return empty($pendingSigners);
×
126
        }
127

128
        private function findNextOrder(array $signRequests, int $completedOrder): ?int {
NEW
129
                $allOrders = array_unique(array_map(fn ($sr) => $sr->getSigningOrder(), $signRequests));
×
NEW
130
                sort($allOrders);
×
131

NEW
132
                foreach ($allOrders as $order) {
×
NEW
133
                        if ($order > $completedOrder) {
×
NEW
134
                                return $order;
×
135
                        }
136
                }
137

NEW
138
                return null;
×
139
        }
140

141
        private function activateSignersForOrder(array $signRequests, int $order): void {
NEW
142
                $signersToActivate = array_filter(
×
NEW
143
                        $signRequests,
×
NEW
144
                        fn ($sr) => $sr->getSigningOrder() === $order
×
NEW
145
                );
×
146

NEW
147
                foreach ($signersToActivate as $signer) {
×
NEW
148
                        if ($signer->getStatusEnum() === SignRequestStatus::DRAFT) {
×
NEW
149
                                $signer->setStatusEnum(SignRequestStatus::ABLE_TO_SIGN);
×
NEW
150
                                $this->signRequestMapper->update($signer);
×
151

NEW
152
                                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signer->getId());
×
NEW
153
                                foreach ($identifyMethods as $methodGroup) {
×
NEW
154
                                        foreach ($methodGroup as $identifyMethod) {
×
NEW
155
                                                $identifyMethod->willNotifyUser(true);
×
NEW
156
                                                $identifyMethod->notify();
×
157
                                        }
158
                                }
159
                        }
160
                }
161
        }
162

163
        private function getSignatureFlow(): SignatureFlow {
164
                $value = $this->appConfig->getValueString(
13✔
165
                        Application::APP_ID,
13✔
166
                        'signature_flow',
13✔
167
                        SignatureFlow::PARALLEL->value
13✔
168
                );
13✔
169

170
                return SignatureFlow::from($value);
13✔
171
        }
172
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc