Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.01% covered (warning)
83.01%
259 / 312
53.12% covered (warning)
53.12%
17 / 32
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProgressService
83.01% covered (warning)
83.01%
259 / 312
53.12% covered (warning)
53.12%
17 / 32
188.42
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
 pollForStatusChange
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 pollForStatusOrErrorChange
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
4
 pollForProgressChange
75.00% covered (warning)
75.00%
21 / 28
0.00% covered (danger)
0.00%
0 / 1
9.00
 pollForStatusChangeInternal
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
11.23
 buildProgressHash
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
6.29
 setSignRequestError
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getSignRequestError
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 clearSignRequestError
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 hasSignRequestError
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasAnySignRequestError
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 setFileError
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getFileError
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 clearFileError
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 buildFileErrorKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 storeSignRequestErrorInMetadata
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
4.77
 getSignRequestErrorFromMetadata
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
5.20
 clearSignRequestErrorInMetadata
46.15% covered (danger)
46.15%
6 / 13
0.00% covered (danger)
0.00%
0 / 1
8.90
 storeFileErrorInMetadata
66.67% covered (warning)
66.67%
10 / 15
0.00% covered (danger)
0.00%
0 / 1
5.93
 getFileErrorFromMetadata
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
6.02
 clearFileErrorInMetadata
41.18% covered (danger)
41.18%
7 / 17
0.00% covered (danger)
0.00%
0 / 1
16.97
 getSignRequestProgress
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getStatusCodeForSignRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isProgressComplete
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 getSingleFileProgressForSignRequest
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
9
 getEnvelopeProgressForSignRequest
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
4
 getFileProgressForSignRequest
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
8
 mapFileProgress
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 mapSignRequestFileProgress
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 mapSignRequestFileProgressWithContext
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 findFileErrorAcrossSignRequests
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
4.12
 getSignRequestStatusCode
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
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\SignRequest;
10
11use OCA\Libresign\Db\File as FileEntity;
12use OCA\Libresign\Db\FileMapper;
13use OCA\Libresign\Db\SignRequest as SignRequestEntity;
14use OCA\Libresign\Db\SignRequestMapper;
15use OCA\Libresign\Enum\FileStatus;
16use OCA\Libresign\Enum\SignRequestStatus;
17use OCP\AppFramework\Db\DoesNotExistException;
18use OCP\ICache;
19use OCP\ICacheFactory;
20
21class ProgressService {
22    private ICache $cache;
23    public const ERROR_KEY_PREFIX = 'libresign_sign_request_error_';
24    public const FILE_ERROR_KEY_PREFIX = 'libresign_file_error_';
25    public const ERROR_CACHE_TTL = 300;
26    /** @var array<string, array> */
27    private array $signRequestErrors = [];
28    /** @var array<string, array> */
29    private array $fileErrors = [];
30
31    public function __construct(
32        private FileMapper $fileMapper,
33        ICacheFactory $cacheFactory,
34        private SignRequestMapper $signRequestMapper,
35        private StatusCacheService $statusCacheService,
36        private ProgressPollDecisionPolicy $pollDecisionPolicy,
37    ) {
38        $this->cache = $cacheFactory->createDistributed('libresign_progress');
39    }
40
41    /**
42     * Poll for status change of a sign request
43     *
44     * Waits up to the specified timeout for the status to change by checking cache
45     *
46     * @return int The current status (changed or original if timeout reached)
47     */
48    public function pollForStatusChange(string $uuid, int $initialStatus, int $timeout = 30, int $intervalSeconds = 1): int {
49        return $this->pollForStatusChangeInternal($uuid, [], $initialStatus, $timeout, $intervalSeconds);
50    }
51
52    public function pollForStatusOrErrorChange(
53        FileEntity $file,
54        SignRequestEntity $signRequest,
55        int $initialStatus,
56        int $timeout = 30,
57        int $intervalSeconds = 1,
58    ): int {
59        if ($file->getNodeType() !== 'envelope') {
60            return $this->pollForProgressChange(
61                $file,
62                $signRequest,
63                [$signRequest->getUuid()],
64                $initialStatus,
65                $timeout,
66                $intervalSeconds,
67            );
68        }
69
70        $signRequestUuids = [$signRequest->getUuid()];
71        $childSignRequests = $this->signRequestMapper
72            ->getByEnvelopeChildrenAndIdentifyMethod($file->getId(), $signRequest->getId());
73        foreach ($childSignRequests as $childSignRequest) {
74            $childUuid = $childSignRequest->getUuid();
75            if ($childUuid !== '') {
76                $signRequestUuids[] = $childUuid;
77            }
78        }
79
80        return $this->pollForProgressChange(
81            $file,
82            $signRequest,
83            $signRequestUuids,
84            $initialStatus,
85            $timeout,
86            $intervalSeconds,
87        );
88    }
89
90    private function pollForProgressChange(
91        FileEntity $file,
92        SignRequestEntity $signRequest,
93        array $errorUuids,
94        int $initialStatus,
95        int $timeout,
96        int $intervalSeconds,
97    ): int {
98        $statusUuid = $file->getUuid();
99        $cachedStatus = $this->statusCacheService->getStatus($statusUuid);
100        $interval = max(1, $intervalSeconds);
101        $initialProgress = $this->getSignRequestProgress($file, $signRequest);
102        $initialHash = $this->buildProgressHash($initialProgress);
103
104        $initialCachedStatus = $this->pollDecisionPolicy->normalizeCachedStatus($cachedStatus);
105        $initialDecision = $this->pollDecisionPolicy->initialStatusFromCache($initialCachedStatus, $initialStatus);
106        if ($initialDecision !== null) {
107            return $initialDecision;
108        }
109
110        for ($elapsed = 0; $elapsed < $timeout; $elapsed += $interval) {
111            if (!empty($errorUuids) && $this->hasAnySignRequestError($errorUuids)) {
112                return $initialStatus;
113            }
114
115            $newCachedStatus = $this->statusCacheService->getStatus($statusUuid);
116            $newCachedStatusValue = $this->pollDecisionPolicy->normalizeCachedStatus($newCachedStatus);
117            $cacheDecision = $this->pollDecisionPolicy->statusFromCacheChange($initialCachedStatus, $newCachedStatusValue);
118            if ($cacheDecision !== null) {
119                return $cacheDecision;
120            }
121
122            $currentProgress = $this->getSignRequestProgress($file, $signRequest);
123            $currentHash = $this->buildProgressHash($currentProgress);
124
125            if ($currentHash !== $initialHash) {
126                return $this->pollDecisionPolicy->statusFromProgressChange(
127                    $newCachedStatusValue,
128                    $initialCachedStatus,
129                    $initialStatus
130                );
131            }
132
133            if ($intervalSeconds > 0) {
134                sleep($intervalSeconds);
135            }
136        }
137
138        return $initialStatus;
139    }
140
141    private function pollForStatusChangeInternal(
142        string $statusUuid,
143        array $errorUuids,
144        int $initialStatus,
145        int $timeout,
146        int $intervalSeconds,
147    ): int {
148        $cachedStatus = $this->statusCacheService->getStatus($statusUuid);
149        $interval = max(1, $intervalSeconds);
150
151        if ($cachedStatus !== false && $cachedStatus !== null && (int)$cachedStatus !== $initialStatus) {
152            return (int)$cachedStatus;
153        }
154
155        for ($elapsed = 0; $elapsed < $timeout; $elapsed += $interval) {
156            if (!empty($errorUuids) && $this->hasAnySignRequestError($errorUuids)) {
157                return $initialStatus;
158            }
159
160            $newCachedStatus = $this->statusCacheService->getStatus($statusUuid);
161            if ($newCachedStatus !== $cachedStatus && $newCachedStatus !== false) {
162                return (int)$newCachedStatus;
163            }
164
165            if ($intervalSeconds > 0) {
166                sleep($intervalSeconds);
167            }
168        }
169
170        return $initialStatus;
171    }
172
173    private function buildProgressHash(array $progress): string {
174        if (!empty($progress['files']) && is_array($progress['files'])) {
175            usort($progress['files'], fn (array $left, array $right): int => ($left['id'] ?? 0) <=> ($right['id'] ?? 0));
176        }
177
178        if (!empty($progress['signers']) && is_array($progress['signers'])) {
179            usort($progress['signers'], fn (array $left, array $right): int => ($left['id'] ?? 0) <=> ($right['id'] ?? 0));
180        }
181
182        return hash('sha256', json_encode($progress, JSON_UNESCAPED_SLASHES) ?: '');
183    }
184
185    public function setSignRequestError(string $uuid, array $error, int $ttl = self::ERROR_CACHE_TTL): void {
186        $this->signRequestErrors[$uuid] = $error;
187        $this->cache->set(self::ERROR_KEY_PREFIX . $uuid, $error, $ttl);
188        $this->storeSignRequestErrorInMetadata($uuid, $error);
189    }
190
191    public function getSignRequestError(string $uuid): ?array {
192        $error = $this->cache->get(self::ERROR_KEY_PREFIX . $uuid);
193        if ($error === false || $error === null) {
194            return $this->signRequestErrors[$uuid]
195                ?? $this->getSignRequestErrorFromMetadata($uuid);
196        }
197        return is_array($error) ? $error : ['message' => (string)$error];
198    }
199
200    public function clearSignRequestError(string $uuid): void {
201        unset($this->signRequestErrors[$uuid]);
202        $this->cache->remove(self::ERROR_KEY_PREFIX . $uuid);
203        $this->clearSignRequestErrorInMetadata($uuid);
204    }
205
206    private function hasSignRequestError(string $uuid): bool {
207        $error = $this->getSignRequestError($uuid);
208        return $error !== null;
209    }
210
211    private function hasAnySignRequestError(array $uuids): bool {
212        foreach ($uuids as $uuid) {
213            if ($uuid !== '' && $this->hasSignRequestError($uuid)) {
214                return true;
215            }
216        }
217        return false;
218    }
219
220    public function setFileError(string $uuid, int $fileId, array $error, int $ttl = self::ERROR_CACHE_TTL): void {
221        $key = $this->buildFileErrorKey($uuid, $fileId);
222        $this->fileErrors[$key] = $error;
223        $this->cache->set($key, $error, $ttl);
224        $this->storeFileErrorInMetadata($uuid, $fileId, $error);
225    }
226
227    public function getFileError(string $uuid, int $fileId): ?array {
228        $key = $this->buildFileErrorKey($uuid, $fileId);
229        $error = $this->cache->get($key);
230        if ($error === false || $error === null) {
231            return $this->fileErrors[$key]
232                ?? $this->getFileErrorFromMetadata($uuid, $fileId);
233        }
234        return is_array($error) ? $error : ['message' => (string)$error];
235    }
236
237    public function clearFileError(string $uuid, int $fileId): void {
238        $key = $this->buildFileErrorKey($uuid, $fileId);
239        unset($this->fileErrors[$key]);
240        $this->cache->remove($key);
241        $this->clearFileErrorInMetadata($uuid, $fileId);
242    }
243
244    private function buildFileErrorKey(string $uuid, int $fileId): string {
245        return self::FILE_ERROR_KEY_PREFIX . $uuid . '_' . $fileId;
246    }
247
248    private function storeSignRequestErrorInMetadata(string $uuid, array $error): void {
249        if ($uuid === '') {
250            return;
251        }
252
253        try {
254            $signRequest = $this->signRequestMapper->getByUuidUncached($uuid);
255        } catch (DoesNotExistException) {
256            return;
257        }
258        if (!$signRequest instanceof SignRequestEntity) {
259            return;
260        }
261
262        $metadata = $signRequest->getMetadata() ?? [];
263        $metadata['libresign_error'] = $error;
264        $signRequest->setMetadata($metadata);
265        $this->signRequestMapper->update($signRequest);
266    }
267
268    private function getSignRequestErrorFromMetadata(string $uuid): ?array {
269        if ($uuid === '') {
270            return null;
271        }
272
273        try {
274            $signRequest = $this->signRequestMapper->getByUuidUncached($uuid);
275        } catch (DoesNotExistException) {
276            return null;
277        }
278        if (!$signRequest instanceof SignRequestEntity) {
279            return null;
280        }
281
282        $metadata = $signRequest->getMetadata() ?? [];
283        $error = $metadata['libresign_error'] ?? null;
284        return is_array($error) ? $error : null;
285    }
286
287    private function clearSignRequestErrorInMetadata(string $uuid): void {
288        if ($uuid === '') {
289            return;
290        }
291
292        try {
293            $signRequest = $this->signRequestMapper->getByUuidUncached($uuid);
294        } catch (DoesNotExistException) {
295            return;
296        }
297        if (!$signRequest instanceof SignRequestEntity) {
298            return;
299        }
300
301        $metadata = $signRequest->getMetadata() ?? [];
302        if (!array_key_exists('libresign_error', $metadata)) {
303            return;
304        }
305
306        unset($metadata['libresign_error']);
307        $signRequest->setMetadata($metadata);
308        $this->signRequestMapper->update($signRequest);
309    }
310
311    private function storeFileErrorInMetadata(string $uuid, int $fileId, array $error): void {
312        if ($uuid === '') {
313            return;
314        }
315
316        try {
317            $signRequest = $this->signRequestMapper->getByUuidUncached($uuid);
318        } catch (DoesNotExistException) {
319            return;
320        }
321        if (!$signRequest instanceof SignRequestEntity) {
322            return;
323        }
324
325        $metadata = $signRequest->getMetadata() ?? [];
326        $fileErrors = $metadata['libresign_file_errors'] ?? [];
327        if (!is_array($fileErrors)) {
328            $fileErrors = [];
329        }
330
331        $fileErrors[$fileId] = $error;
332        $metadata['libresign_file_errors'] = $fileErrors;
333        $signRequest->setMetadata($metadata);
334        $this->signRequestMapper->update($signRequest);
335    }
336
337    private function getFileErrorFromMetadata(string $uuid, int $fileId): ?array {
338        if ($uuid === '') {
339            return null;
340        }
341
342        try {
343            $signRequest = $this->signRequestMapper->getByUuidUncached($uuid);
344        } catch (DoesNotExistException) {
345            return null;
346        }
347        if (!$signRequest instanceof SignRequestEntity) {
348            return null;
349        }
350
351        $metadata = $signRequest->getMetadata() ?? [];
352        $fileErrors = $metadata['libresign_file_errors'] ?? null;
353        if (!is_array($fileErrors)) {
354            return null;
355        }
356
357        $error = $fileErrors[$fileId] ?? null;
358        return is_array($error) ? $error : null;
359    }
360
361    private function clearFileErrorInMetadata(string $uuid, int $fileId): void {
362        if ($uuid === '') {
363            return;
364        }
365
366        try {
367            $signRequest = $this->signRequestMapper->getByUuidUncached($uuid);
368        } catch (DoesNotExistException) {
369            return;
370        }
371        if (!$signRequest instanceof SignRequestEntity) {
372            return;
373        }
374
375        $metadata = $signRequest->getMetadata() ?? [];
376        $fileErrors = $metadata['libresign_file_errors'] ?? null;
377        if (!is_array($fileErrors) || !array_key_exists($fileId, $fileErrors)) {
378            return;
379        }
380
381        unset($fileErrors[$fileId]);
382        if (empty($fileErrors)) {
383            unset($metadata['libresign_file_errors']);
384        } else {
385            $metadata['libresign_file_errors'] = $fileErrors;
386        }
387        $signRequest->setMetadata($metadata);
388        $this->signRequestMapper->update($signRequest);
389    }
390
391    /**
392     * Get progress for a specific sign request
393     *
394     * Returns progress data tailored to the specific sign request,
395     * not the global file status
396     *
397     * @return array<string, mixed> Progress data with structure: {total, signed, pending, files?, signers?}
398     */
399    public function getSignRequestProgress(FileEntity $file, SignRequestEntity $signRequest): array {
400        return match (true) {
401            $file->getNodeType() === 'envelope' => $this->getEnvelopeProgressForSignRequest($file, $signRequest),
402            !$file->getParentFileId() => $this->getSingleFileProgressForSignRequest($file, $signRequest),
403            default => $this->getFileProgressForSignRequest($file, $signRequest),
404        };
405    }
406
407    public function getStatusCodeForSignRequest(FileEntity $file, SignRequestEntity $signRequest): int {
408        return $this->getSignRequestStatusCode($file, $signRequest);
409    }
410
411    public function isProgressComplete(array $progress): bool {
412        $total = (int)($progress['total'] ?? 0);
413        if ($total <= 0) {
414            return false;
415        }
416        $signed = (int)($progress['signed'] ?? 0);
417        $pending = (int)($progress['pending'] ?? 0);
418        $inProgress = (int)($progress['inProgress'] ?? 0);
419        $errors = (int)($progress['errors'] ?? 0);
420        return ($signed + $errors) >= $total && $pending <= 0 && $inProgress <= 0;
421    }
422
423    /**
424     * Get progress for a sign request on a single file
425     *
426     * Returns counts relative to the sign request status
427     *
428     * @return array<string, mixed>
429     */
430    public function getSingleFileProgressForSignRequest(FileEntity $file, SignRequestEntity $signRequest): array {
431        $statusCode = $this->getSignRequestStatusCode($file, $signRequest);
432        $isSigned = $statusCode === FileStatus::SIGNED->value;
433        $isInProgress = $statusCode === FileStatus::SIGNING_IN_PROGRESS->value;
434        $fileError = $this->getFileError($signRequest->getUuid(), $file->getId());
435        $hasError = $fileError !== null;
436
437        return [
438            'total' => 1,
439            'signed' => $isSigned ? 1 : 0,
440            'inProgress' => $hasError ? 0 : ($isInProgress ? 1 : 0),
441            'errors' => $hasError ? 1 : 0,
442            'pending' => $hasError || $isSigned || $isInProgress ? 0 : 1,
443            'files' => [
444                array_merge(
445                    [
446                        'id' => $file->getId(),
447                        'name' => $file->getName(),
448                        'status' => $statusCode,
449                        'statusText' => $this->fileMapper->getTextOfStatus($statusCode),
450                    ],
451                    $hasError ? ['error' => $fileError] : []
452                )
453            ],
454        ];
455    }
456
457    /**
458     * Get progress for a sign request on an envelope
459     *
460     * Returns progress for all files in the envelope relative to this signer
461     *
462     * @return array<string, mixed>
463     */
464    public function getEnvelopeProgressForSignRequest(FileEntity $envelope, SignRequestEntity $signRequest): array {
465        $children = $this->fileMapper->getChildrenFiles($envelope->getId());
466        if (empty($children)) {
467            $children = [$envelope];
468        }
469
470        $childSignRequests = $this->signRequestMapper
471            ->getByEnvelopeChildrenAndIdentifyMethod($envelope->getId(), $signRequest->getId());
472        $childSignRequestsByFileId = [];
473        foreach ($childSignRequests as $childSignRequest) {
474            $fileId = $childSignRequest->getFileId();
475            if ($fileId !== null) {
476                $childSignRequestsByFileId[$fileId] = $childSignRequest;
477            }
478        }
479
480        $files = array_map(function (FileEntity $child) use ($signRequest, $childSignRequestsByFileId): array {
481            $childSignRequest = $childSignRequestsByFileId[$child->getId()] ?? null;
482            return $this->mapSignRequestFileProgressWithContext($child, $signRequest, $childSignRequest);
483        }, $children);
484        $total = count($files);
485        $signed = count(array_filter($files, fn (array $file) => $file['status'] === FileStatus::SIGNED->value));
486        $inProgress = count(array_filter($files, fn (array $file) => $file['status'] === FileStatus::SIGNING_IN_PROGRESS->value));
487        $errors = count(array_filter($files, fn (array $file) => !empty($file['error'])));
488        $pending = max(0, $total - $signed - $inProgress - $errors);
489
490        return [
491            'total' => $total,
492            'signed' => $signed,
493            'inProgress' => $inProgress,
494            'errors' => $errors,
495            'pending' => $pending,
496            'files' => $files,
497        ];
498    }
499
500    /**
501     * Get progress for a sign request on a child file in an envelope
502     *
503     * @return array<string, mixed>
504     */
505    public function getFileProgressForSignRequest(FileEntity $file, SignRequestEntity $signRequest): array {
506        $statusCode = $this->getSignRequestStatusCode($file, $signRequest);
507        $isSigned = $statusCode === FileStatus::SIGNED->value;
508        $isInProgress = $statusCode === FileStatus::SIGNING_IN_PROGRESS->value;
509        $fileError = $this->getFileError($signRequest->getUuid(), $file->getId());
510        $hasError = $fileError !== null;
511
512        return [
513            'total' => 1,
514            'signed' => $isSigned ? 1 : 0,
515            'inProgress' => $hasError ? 0 : ($isInProgress ? 1 : 0),
516            'errors' => $hasError ? 1 : 0,
517            'pending' => $hasError || $isSigned || $isInProgress ? 0 : 1,
518            'signers' => [
519                [
520                    'id' => $signRequest->getId(),
521                    'displayName' => $signRequest->getDisplayName(),
522                    'signed' => $signRequest->getSigned()?->format('c'),
523                    'status' => $statusCode,
524                ]
525            ],
526        ];
527    }
528
529    /**
530     * Map file progress data
531     *
532     * @return array<string, mixed>
533     */
534    private function mapFileProgress(FileEntity $file): array {
535        return [
536            'id' => $file->getId(),
537            'name' => $file->getName(),
538            'status' => $file->getStatus(),
539            'statusText' => $this->fileMapper->getTextOfStatus($file->getStatus()),
540        ];
541    }
542
543    private function mapSignRequestFileProgress(FileEntity $file, SignRequestEntity $signRequest): array {
544        $statusCode = $this->getSignRequestStatusCode($file, $signRequest);
545        $error = $this->getFileError($signRequest->getUuid(), $file->getId());
546
547        $mapped = [
548            'id' => $file->getId(),
549            'name' => $file->getName(),
550            'status' => $statusCode,
551            'statusText' => $this->fileMapper->getTextOfStatus($statusCode),
552        ];
553
554        if ($error !== null) {
555            $mapped['error'] = $error;
556        }
557
558        return $mapped;
559    }
560
561    private function mapSignRequestFileProgressWithContext(FileEntity $file, SignRequestEntity $defaultSignRequest, ?SignRequestEntity $childSignRequest): array {
562        $effectiveSignRequest = $childSignRequest ?? $defaultSignRequest;
563        $statusCode = $this->getSignRequestStatusCode($file, $effectiveSignRequest);
564        $errorUuid = $childSignRequest?->getUuid() ?? $defaultSignRequest->getUuid();
565        $error = $this->getFileError($errorUuid, $file->getId());
566        if ($error === null) {
567            $error = $this->findFileErrorAcrossSignRequests($file->getId());
568        }
569
570        $mapped = [
571            'id' => $file->getId(),
572            'name' => $file->getName(),
573            'status' => $statusCode,
574            'statusText' => $this->fileMapper->getTextOfStatus($statusCode),
575        ];
576
577        if ($error !== null) {
578            $mapped['error'] = $error;
579        }
580
581        return $mapped;
582    }
583
584    private function findFileErrorAcrossSignRequests(int $fileId): ?array {
585        $signRequests = $this->signRequestMapper->getByFileId($fileId);
586        foreach ($signRequests as $signRequest) {
587            $error = $this->getFileError($signRequest->getUuid(), $fileId);
588            if ($error !== null) {
589                return $error;
590            }
591        }
592        return null;
593    }
594
595    private function getSignRequestStatusCode(FileEntity $file, SignRequestEntity $signRequest): int {
596        if ($signRequest->getSigned() !== null) {
597            return FileStatus::SIGNED->value;
598        }
599
600        if ($file->getStatus() === FileStatus::SIGNING_IN_PROGRESS->value) {
601            return FileStatus::SIGNING_IN_PROGRESS->value;
602        }
603
604        return match ($signRequest->getStatusEnum()) {
605            SignRequestStatus::DRAFT => FileStatus::DRAFT->value,
606            SignRequestStatus::ABLE_TO_SIGN => FileStatus::ABLE_TO_SIGN->value,
607            default => $file->getStatus(),
608        };
609    }
610}