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

LibreSign / libresign / 20175673268

12 Dec 2025 06:07PM UTC coverage: 43.865%. First build
20175673268

Pull #6163

github

web-flow
Merge ede3fe8b9 into 9c6cebf5f
Pull Request #6163: refactor: move status validation to sequential service

10 of 35 new or added lines in 2 files covered. (28.57%)

5788 of 13195 relevant lines covered (43.87%)

5.11 hits per line

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

16.67
/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\Enum\SignatureFlow;
14
use OCA\Libresign\Enum\SignRequestStatus;
15
use OCP\IAppConfig;
16

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

126
                return empty($pendingSigners);
×
127
        }
128

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

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

139
                return null;
×
140
        }
141

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

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

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

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

171
                return SignatureFlow::from($value);
13✔
172
        }
173

174
        /**
175
         * Check if there are signers with lower signing order that haven't signed yet
176
         */
177
        public function hasPendingLowerOrderSigners(int $fileId, int $currentOrder): bool {
NEW
178
                $signRequests = $this->signRequestMapper->getByFileId($fileId);
×
179

NEW
180
                foreach ($signRequests as $signRequest) {
×
NEW
181
                        $order = $signRequest->getSigningOrder();
×
NEW
182
                        $status = $signRequest->getStatusEnum();
×
183

184
                        // If a signer with lower order hasn't signed yet, return true
NEW
185
                        if ($order < $currentOrder && $status !== SignRequestStatus::SIGNED) {
×
NEW
186
                                return true;
×
187
                        }
188
                }
189

NEW
190
                return false;
×
191
        }
192

193
        /**
194
         * Check if changing from currentStatus to desiredStatus is an upgrade (or same level)
195
         * Status hierarchy: DRAFT (0) < ABLE_TO_SIGN (1) < SIGNED (2)
196
         */
197
        public function isStatusUpgrade(
198
                SignRequestStatus $currentStatus,
199
                SignRequestStatus $desiredStatus,
200
        ): bool {
NEW
201
                return $desiredStatus->value >= $currentStatus->value;
×
202
        }
203

204
        /**
205
         * Validate if a signer can transition to ABLE_TO_SIGN status based on signing order
206
         * In ordered numeric flow, prevents skipping ahead if lower-order signers haven't signed
207
         *
208
         * @param SignRequestStatus $desiredStatus The status being requested
209
         * @param int $signingOrder The signer's order
210
         * @param int $fileId The file ID
211
         * @return SignRequestStatus The validated status (may return DRAFT if validation fails)
212
         */
213
        public function validateStatusByOrder(
214
                SignRequestStatus $desiredStatus,
215
                int $signingOrder,
216
                int $fileId,
217
        ): SignRequestStatus {
218
                // Only validate for ordered numeric flow
NEW
219
                if (!$this->isOrderedNumericFlow()) {
×
NEW
220
                        return $desiredStatus;
×
221
                }
222

223
                // Only validate when trying to set ABLE_TO_SIGN and not the first signer
NEW
224
                if ($desiredStatus !== SignRequestStatus::ABLE_TO_SIGN || $signingOrder <= 1) {
×
NEW
225
                        return $desiredStatus;
×
226
                }
227

228
                // Check if any lower order signers haven't signed yet
NEW
229
                if ($this->hasPendingLowerOrderSigners($fileId, $signingOrder)) {
×
NEW
230
                        return SignRequestStatus::DRAFT;
×
231
                }
232

NEW
233
                return $desiredStatus;
×
234
        }
235
}
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