From aaaf7339b21efb46751bc497e475a6ea364d2ac3 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:57:57 -0300 Subject: [PATCH 01/18] feat: add numeric conversion methods to SignatureFlow enum Add toNumeric() and fromNumeric() methods to enable conversion between SignatureFlow enum values and their integer database representations. - PARALLEL = 1 - ORDERED_NUMERIC = 2 This allows storing signature flow as integers in the database while maintaining type-safe enum usage in the code. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Enum/SignatureFlow.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/Enum/SignatureFlow.php b/lib/Enum/SignatureFlow.php index ba4d99383c..ba23e5e159 100644 --- a/lib/Enum/SignatureFlow.php +++ b/lib/Enum/SignatureFlow.php @@ -15,4 +15,29 @@ enum SignatureFlow: string { case PARALLEL = 'parallel'; case ORDERED_NUMERIC = 'ordered_numeric'; + + /** + * Convert enum to numeric value for database storage + * @return int 1 for PARALLEL, 2 for ORDERED_NUMERIC + */ + public function toNumeric(): int { + return match($this) { + self::PARALLEL => 1, + self::ORDERED_NUMERIC => 2, + }; + } + + /** + * Create enum from numeric database value + * @param int $value 1 for PARALLEL, 2 for ORDERED_NUMERIC + * @return self + * @throws \ValueError if value is invalid + */ + public static function fromNumeric(int $value): self { + return match($value) { + 1 => self::PARALLEL, + 2 => self::ORDERED_NUMERIC, + default => throw new \ValueError("Invalid numeric value for SignatureFlow: $value"), + }; + } } From 9c290d7302fd7ca73b2fac644139853086127baa Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:58:18 -0300 Subject: [PATCH 02/18] feat: add signature_flow column to libresign_file table Add signature_flow column to store signature flow mode at file level. - Column type: SMALLINT NOT NULL DEFAULT 1 - 1 = parallel (default) - 2 = ordered_numeric This enables per-file configuration of signature flow, overriding the global application setting when specified. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Migration/Version15000Date20251209000000.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/Migration/Version15000Date20251209000000.php b/lib/Migration/Version15000Date20251209000000.php index 064a454fc3..230d71d5ec 100644 --- a/lib/Migration/Version15000Date20251209000000.php +++ b/lib/Migration/Version15000Date20251209000000.php @@ -55,6 +55,17 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt } } + if ($schema->hasTable('libresign_file')) { + $tableFile = $schema->getTable('libresign_file'); + if (!$tableFile->hasColumn('signature_flow')) { + $tableFile->addColumn('signature_flow', Types::SMALLINT, [ + 'notnull' => true, + 'default' => 1, + 'comment' => 'Signature flow mode: 1=parallel, 2=ordered_numeric', + ]); + } + } + return $schema; } From 064b4cfc7e97bb935247a1a17d9d9f1cc000a6c8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:58:51 -0300 Subject: [PATCH 03/18] feat: add signature flow support to File entity Add signatureFlow property and enum conversion helper methods to File entity. - New protected property: signatureFlow (SMALLINT, default 1) - getSignatureFlowEnum(): Returns SignatureFlow enum from integer - setSignatureFlowEnum(): Sets integer from SignatureFlow enum These methods provide type-safe access to signature flow configuration stored in the database. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Db/File.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/Db/File.php b/lib/Db/File.php index ab92b067e9..dcf6da27ae 100644 --- a/lib/Db/File.php +++ b/lib/Db/File.php @@ -38,6 +38,8 @@ * @method ?array getMetadata() * @method void setModificationStatus(int $modificationStatus) * @method int getModificationStatus() + * @method void setSignatureFlow(int $signatureFlow) + * @method int getSignatureFlow() */ class File extends Entity { protected int $nodeId = 0; @@ -52,6 +54,7 @@ class File extends Entity { protected ?string $callback = null; protected ?array $metadata = null; protected int $modificationStatus = 0; + protected int $signatureFlow = 1; public const STATUS_NOT_LIBRESIGN_FILE = -1; public const STATUS_DRAFT = 0; public const STATUS_ABLE_TO_SIGN = 1; @@ -78,6 +81,7 @@ public function __construct() { $this->addType('status', Types::INTEGER); $this->addType('metadata', Types::JSON); $this->addType('modificationStatus', Types::SMALLINT); + $this->addType('signatureFlow', Types::SMALLINT); } public function isDeletedAccount(): bool { @@ -89,4 +93,20 @@ public function getUserId(): string { $metadata = $this->getMetadata(); return $metadata['deleted_account']['account'] ?? $this->userId ?? ''; } + + /** + * Get signature flow as enum + * @return \OCA\Libresign\Enum\SignatureFlow + */ + public function getSignatureFlowEnum(): \OCA\Libresign\Enum\SignatureFlow { + return \OCA\Libresign\Enum\SignatureFlow::fromNumeric($this->signatureFlow); + } + + /** + * Set signature flow from enum + * @param \OCA\Libresign\Enum\SignatureFlow $flow + */ + public function setSignatureFlowEnum(\OCA\Libresign\Enum\SignatureFlow $flow): void { + $this->setSignatureFlow($flow->toNumeric()); + } } From fd8041285abf848223534d1bf673cfd99163cd54 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:59:14 -0300 Subject: [PATCH 04/18] refactor: use file-level signature flow in SequentialSigningService Replace global config dependency with file-level signature flow. - Add setFile() method to inject File entity - Replace getSignatureFlow() to read from file instead of IAppConfig - Remove IAppConfig dependency (no longer needed) - Add validation: throws LogicException if getSignatureFlow() called without file This enables per-file signature flow configuration while maintaining backward compatibility through database default values. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/SequentialSigningService.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/Service/SequentialSigningService.php b/lib/Service/SequentialSigningService.php index a639cd00cc..90feb3abb9 100644 --- a/lib/Service/SequentialSigningService.php +++ b/lib/Service/SequentialSigningService.php @@ -8,7 +8,7 @@ namespace OCA\Libresign\Service; -use OCA\Libresign\AppInfo\Application; +use OCA\Libresign\Db\File as FileEntity; use OCA\Libresign\Db\SignRequestMapper; use OCA\Libresign\Enum\SignatureFlow; use OCA\Libresign\Enum\SignRequestStatus; @@ -16,6 +16,7 @@ class SequentialSigningService { private int $currentOrder = 1; + private ?FileEntity $file = null; public function __construct( private IAppConfig $appConfig, @@ -24,9 +25,11 @@ public function __construct( ) { } - /** - * Check if ordered numeric flow is enabled - */ + public function setFile(FileEntity $file): self { + $this->file = $file; + return $this; + } + public function isOrderedNumericFlow(): bool { return $this->getSignatureFlow() === SignatureFlow::ORDERED_NUMERIC; } @@ -162,13 +165,10 @@ private function activateSignersForOrder(array $signRequests, int $order): void } private function getSignatureFlow(): SignatureFlow { - $value = $this->appConfig->getValueString( - Application::APP_ID, - 'signature_flow', - SignatureFlow::PARALLEL->value - ); - - return SignatureFlow::from($value); + if ($this->file === null) { + throw new \LogicException('File must be set before calling getSignatureFlow(). Call setFile() first.'); + } + return $this->file->getSignatureFlowEnum(); } /** From 80392b953525866e7d98e410240473af5d4927e4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:00:53 -0300 Subject: [PATCH 05/18] feat: add signatureFlow parameter to request-signature endpoint Add optional signatureFlow parameter to POST /request-signature API. - Accepts 'parallel' or 'ordered_numeric' values - Falls back to global configuration if not provided - Documented in PHPDoc and OpenAPI annotations This enables clients to specify signature flow mode per document when creating signature requests. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Controller/RequestSignatureController.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/Controller/RequestSignatureController.php b/lib/Controller/RequestSignatureController.php index 07fac53703..e436cfdbee 100644 --- a/lib/Controller/RequestSignatureController.php +++ b/lib/Controller/RequestSignatureController.php @@ -56,6 +56,7 @@ public function __construct( * @param string $name The name of file to sign * @param string|null $callback URL that will receive a POST after the document is signed * @param integer|null $status Numeric code of status * 0 - no signers * 1 - signed * 2 - pending + * @param string|null $signatureFlow Signature flow mode: 'parallel' or 'ordered_numeric'. If not provided, uses global configuration * @return DataResponse|DataResponse}, array{}> * * 200: OK @@ -65,7 +66,14 @@ public function __construct( #[NoCSRFRequired] #[RequireManager] #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/request-signature', requirements: ['apiVersion' => '(v1)'])] - public function request(array $file, array $users, string $name, ?string $callback = null, ?int $status = 1): DataResponse { + public function request( + array $file, + array $users, + string $name, + ?string $callback = null, + ?int $status = 1, + ?string $signatureFlow = null, + ): DataResponse { $user = $this->userSession->getUser(); $data = [ 'file' => $file, @@ -73,7 +81,8 @@ public function request(array $file, array $users, string $name, ?string $callba 'users' => $users, 'status' => $status, 'callback' => $callback, - 'userManager' => $user + 'userManager' => $user, + 'signatureFlow' => $signatureFlow, ]; try { $this->requestSignatureService->validateNewRequestToFile($data); From 8ed4b3c7c51cb6be70577202638533fa6226c5bb Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:01:13 -0300 Subject: [PATCH 06/18] feat: implement signature flow handling in RequestSignatureService Add signature flow parameter processing and file configuration logic. - Parse signatureFlow from request data and convert to enum - Fall back to global config if parameter invalid or missing - Set file entity signature flow via setSignatureFlowEnum() - Initialize SequentialSigningService with file entity context - Add IAppConfig dependency for global fallback This completes the backend logic for per-file signature flow configuration with graceful fallback to system defaults. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/RequestSignatureService.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/Service/RequestSignatureService.php b/lib/Service/RequestSignatureService.php index 2da4aa1514..d65d900c83 100644 --- a/lib/Service/RequestSignatureService.php +++ b/lib/Service/RequestSignatureService.php @@ -8,12 +8,14 @@ namespace OCA\Libresign\Service; +use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Db\File as FileEntity; use OCA\Libresign\Db\FileElementMapper; use OCA\Libresign\Db\FileMapper; use OCA\Libresign\Db\IdentifyMethodMapper; use OCA\Libresign\Db\SignRequest as SignRequestEntity; use OCA\Libresign\Db\SignRequestMapper; +use OCA\Libresign\Enum\SignatureFlow; use OCA\Libresign\Handler\DocMdpHandler; use OCA\Libresign\Helper\ValidateHelper; use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod; @@ -21,6 +23,7 @@ use OCP\Files\IMimeTypeDetector; use OCP\Files\Node; use OCP\Http\Client\IClientService; +use OCP\IAppConfig; use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; @@ -47,6 +50,7 @@ public function __construct( protected DocMdpHandler $docMdpHandler, protected LoggerInterface $logger, protected SequentialSigningService $sequentialSigningService, + protected IAppConfig $appConfig, ) { } @@ -56,6 +60,7 @@ public function save(array $data): FileEntity { if (!isset($data['status'])) { $data['status'] = $file->getStatus(); } + $this->sequentialSigningService->setFile($file); $this->associateToSigners($data, $file->getId()); return $file; } @@ -106,10 +111,28 @@ public function saveFile(array $data): FileEntity { } else { $file->setStatus(FileEntity::STATUS_ABLE_TO_SIGN); } + + if (isset($data['signatureFlow']) && is_string($data['signatureFlow'])) { + try { + $signatureFlow = \OCA\Libresign\Enum\SignatureFlow::from($data['signatureFlow']); + $file->setSignatureFlowEnum($signatureFlow); + } catch (\ValueError) { + $this->setSignatureFlowFromGlobalConfig($file); + } + } else { + $this->setSignatureFlowFromGlobalConfig($file); + } + $this->fileMapper->insert($file); return $file; } + private function setSignatureFlowFromGlobalConfig(FileEntity $file): void { + $globalFlowValue = $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', SignatureFlow::PARALLEL->value); + $globalFlow = SignatureFlow::from($globalFlowValue); + $file->setSignatureFlowEnum($globalFlow); + } + private function updateStatus(FileEntity $file, int $status): FileEntity { if ($status > $file->getStatus()) { $file->setStatus($status); From b7b73ec87a63d3915f11594c6d0cf41926f70294 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:01:47 -0300 Subject: [PATCH 07/18] refactor: pass file entity to SequentialSigningService in SignFileService Update releaseNextOrder call to provide file entity context. - Call setFile() before releaseNextOrder() - Enables SequentialSigningService to read file-level signature flow This ensures signature flow logic uses the correct per-file configuration during document signing workflow. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/SignFileService.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/Service/SignFileService.php b/lib/Service/SignFileService.php index c67ff9f46d..5c167e7813 100644 --- a/lib/Service/SignFileService.php +++ b/lib/Service/SignFileService.php @@ -380,10 +380,12 @@ protected function updateSignRequest(string $hash): void { $this->signRequestMapper->update($this->signRequest); - $this->sequentialSigningService->releaseNextOrder( - $this->signRequest->getFileId(), - $this->signRequest->getSigningOrder() - ); + $this->sequentialSigningService + ->setFile($this->libreSignFile) + ->releaseNextOrder( + $this->signRequest->getFileId(), + $this->signRequest->getSigningOrder() + ); } protected function updateLibreSignFile(string $hash): void { From 281b05811de1fab4b3d44d8ce5fa3d0380f02a77 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:02:18 -0300 Subject: [PATCH 08/18] feat: include signatureFlow in API responses Expose signature flow configuration in file list and detail endpoints. FileService changes: - Add signatureFlow to fileData object in formatFile() SignRequestMapper changes: - Include f.signature_flow in SELECT query - Convert numeric value to enum string in formatListRow() - Remove signature_flow from response (cleanup after conversion) This enables frontend to display and use per-file signature flow configuration. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Db/SignRequestMapper.php | 5 +++++ lib/Service/FileService.php | 1 + 2 files changed, 6 insertions(+) diff --git a/lib/Db/SignRequestMapper.php b/lib/Db/SignRequestMapper.php index 51d5e9b249..8fddbebe17 100644 --- a/lib/Db/SignRequestMapper.php +++ b/lib/Db/SignRequestMapper.php @@ -9,6 +9,7 @@ namespace OCA\Libresign\Db; use DateTimeInterface; +use OCA\Libresign\Enum\SignatureFlow; use OCA\Libresign\Enum\SignRequestStatus; use OCA\Libresign\Helper\Pagination; use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod; @@ -499,6 +500,7 @@ private function getFilesAssociatedFilesWithMeQueryBuilder(string $userId, array 'f.status', 'f.metadata', 'f.created_at', + 'f.signature_flow', ) ->groupBy( 'f.id', @@ -509,6 +511,7 @@ private function getFilesAssociatedFilesWithMeQueryBuilder(string $userId, array 'f.name', 'f.status', 'f.created_at', + 'f.signature_flow', ); // metadata is a json column, the right way is to use f.metadata::text // when the database is PostgreSQL. The problem is that the command @@ -619,11 +622,13 @@ private function formatListRow(array $row): array { $row['nodeId'] = (int)$row['node_id']; $row['name'] = $this->removeExtensionFromName($row['name'], $row['metadata']); + $row['signatureFlow'] = SignatureFlow::fromNumeric((int)($row['signature_flow']))->value; unset( $row['user_id'], $row['node_id'], $row['signed_node_id'], + $row['signature_flow'], ); return $row; } diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index 1aafd9ff89..e75cb9f845 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -711,6 +711,7 @@ private function loadLibreSignData(): void { $this->fileData->created_at = $this->file->getCreatedAt()->format(DateTimeInterface::ATOM); $this->fileData->statusText = $this->fileMapper->getTextOfStatus($this->file->getStatus()); $this->fileData->nodeId = $this->file->getNodeId(); + $this->fileData->signatureFlow = $this->file->getSignatureFlow(); $this->fileData->requested_by = [ 'userId' => $this->file->getUserId(), From 56d3d428019d4d832f3fe149818c44714c3c9e87 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:02:39 -0300 Subject: [PATCH 09/18] docs: add signatureFlow parameter to OpenAPI specification Update API documentation for request-signature endpoint. - Add signatureFlow parameter to request body schema - Type: string, nullable - Description documents accepted values and fallback behavior This ensures API consumers have accurate documentation for the signature flow parameter. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- openapi-full.json | 5 +++++ openapi.json | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/openapi-full.json b/openapi-full.json index b4eb7cf6e2..2d9b7aaa9b 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -6456,6 +6456,11 @@ "nullable": true, "default": 1, "description": "Numeric code of status * 0 - no signers * 1 - signed * 2 - pending" + }, + "signatureFlow": { + "type": "string", + "nullable": true, + "description": "Signature flow mode: 'parallel' or 'ordered_numeric'. If not provided, uses global configuration" } } } diff --git a/openapi.json b/openapi.json index 65d463c12c..1d618793e3 100644 --- a/openapi.json +++ b/openapi.json @@ -6306,6 +6306,11 @@ "nullable": true, "default": 1, "description": "Numeric code of status * 0 - no signers * 1 - signed * 2 - pending" + }, + "signatureFlow": { + "type": "string", + "nullable": true, + "description": "Signature flow mode: 'parallel' or 'ordered_numeric'. If not provided, uses global configuration" } } } From 80cc5d867b8c809ab0467cedb5f7073b9d12c141 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:03:36 -0300 Subject: [PATCH 10/18] chore: remove unecessary docblock Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Db/File.php | 8 -------- lib/Enum/SignatureFlow.php | 10 ---------- 2 files changed, 18 deletions(-) diff --git a/lib/Db/File.php b/lib/Db/File.php index dcf6da27ae..b5ad56c593 100644 --- a/lib/Db/File.php +++ b/lib/Db/File.php @@ -94,18 +94,10 @@ public function getUserId(): string { return $metadata['deleted_account']['account'] ?? $this->userId ?? ''; } - /** - * Get signature flow as enum - * @return \OCA\Libresign\Enum\SignatureFlow - */ public function getSignatureFlowEnum(): \OCA\Libresign\Enum\SignatureFlow { return \OCA\Libresign\Enum\SignatureFlow::fromNumeric($this->signatureFlow); } - /** - * Set signature flow from enum - * @param \OCA\Libresign\Enum\SignatureFlow $flow - */ public function setSignatureFlowEnum(\OCA\Libresign\Enum\SignatureFlow $flow): void { $this->setSignatureFlow($flow->toNumeric()); } diff --git a/lib/Enum/SignatureFlow.php b/lib/Enum/SignatureFlow.php index ba23e5e159..2c60ddeab5 100644 --- a/lib/Enum/SignatureFlow.php +++ b/lib/Enum/SignatureFlow.php @@ -16,10 +16,6 @@ enum SignatureFlow: string { case PARALLEL = 'parallel'; case ORDERED_NUMERIC = 'ordered_numeric'; - /** - * Convert enum to numeric value for database storage - * @return int 1 for PARALLEL, 2 for ORDERED_NUMERIC - */ public function toNumeric(): int { return match($this) { self::PARALLEL => 1, @@ -27,12 +23,6 @@ public function toNumeric(): int { }; } - /** - * Create enum from numeric database value - * @param int $value 1 for PARALLEL, 2 for ORDERED_NUMERIC - * @return self - * @throws \ValueError if value is invalid - */ public static function fromNumeric(int $value): self { return match($value) { 1 => self::PARALLEL, From 90e385335181fae29d9b35e334e5b354753a3eae Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:14:12 -0300 Subject: [PATCH 11/18] refactor: move signature flow constants to enum Move database value constants from File entity to SignatureFlow enum where they belong as the single source of truth. - Add SignatureFlow::NUMERIC_PARALLEL and NUMERIC_ORDERED_NUMERIC - Remove circular dependency between File and SignatureFlow - Update all references to use enum constants - Use descriptive names instead of abbreviations This follows single responsibility principle: the enum owns its database representation. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Db/File.php | 3 +- lib/Enum/SignatureFlow.php | 11 +++-- .../Version15000Date20251209000000.php | 3 +- tests/php/Unit/Enum/SignatureFlowTest.php | 46 +++++++++++++++++++ 4 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 tests/php/Unit/Enum/SignatureFlowTest.php diff --git a/lib/Db/File.php b/lib/Db/File.php index b5ad56c593..856df3dc3e 100644 --- a/lib/Db/File.php +++ b/lib/Db/File.php @@ -8,6 +8,7 @@ namespace OCA\Libresign\Db; +use OCA\Libresign\Enum\SignatureFlow; use OCP\AppFramework\Db\Entity; use OCP\DB\Types; @@ -54,7 +55,7 @@ class File extends Entity { protected ?string $callback = null; protected ?array $metadata = null; protected int $modificationStatus = 0; - protected int $signatureFlow = 1; + protected int $signatureFlow = SignatureFlow::NUMERIC_PARALLEL; public const STATUS_NOT_LIBRESIGN_FILE = -1; public const STATUS_DRAFT = 0; public const STATUS_ABLE_TO_SIGN = 1; diff --git a/lib/Enum/SignatureFlow.php b/lib/Enum/SignatureFlow.php index 2c60ddeab5..71d31ba538 100644 --- a/lib/Enum/SignatureFlow.php +++ b/lib/Enum/SignatureFlow.php @@ -16,17 +16,20 @@ enum SignatureFlow: string { case PARALLEL = 'parallel'; case ORDERED_NUMERIC = 'ordered_numeric'; + public const NUMERIC_PARALLEL = 1; + public const NUMERIC_ORDERED_NUMERIC = 2; + public function toNumeric(): int { return match($this) { - self::PARALLEL => 1, - self::ORDERED_NUMERIC => 2, + self::PARALLEL => self::NUMERIC_PARALLEL, + self::ORDERED_NUMERIC => self::NUMERIC_ORDERED_NUMERIC, }; } public static function fromNumeric(int $value): self { return match($value) { - 1 => self::PARALLEL, - 2 => self::ORDERED_NUMERIC, + self::NUMERIC_PARALLEL => self::PARALLEL, + self::NUMERIC_ORDERED_NUMERIC => self::ORDERED_NUMERIC, default => throw new \ValueError("Invalid numeric value for SignatureFlow: $value"), }; } diff --git a/lib/Migration/Version15000Date20251209000000.php b/lib/Migration/Version15000Date20251209000000.php index 230d71d5ec..66f3332784 100644 --- a/lib/Migration/Version15000Date20251209000000.php +++ b/lib/Migration/Version15000Date20251209000000.php @@ -9,6 +9,7 @@ namespace OCA\Libresign\Migration; use Closure; +use OCA\Libresign\Enum\SignatureFlow; use OCP\DB\ISchemaWrapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\Types; @@ -60,7 +61,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt if (!$tableFile->hasColumn('signature_flow')) { $tableFile->addColumn('signature_flow', Types::SMALLINT, [ 'notnull' => true, - 'default' => 1, + 'default' => SignatureFlow::NUMERIC_PARALLEL, 'comment' => 'Signature flow mode: 1=parallel, 2=ordered_numeric', ]); } diff --git a/tests/php/Unit/Enum/SignatureFlowTest.php b/tests/php/Unit/Enum/SignatureFlowTest.php new file mode 100644 index 0000000000..b3bc8ed998 --- /dev/null +++ b/tests/php/Unit/Enum/SignatureFlowTest.php @@ -0,0 +1,46 @@ + [SignatureFlow::PARALLEL, SignatureFlow::NUMERIC_PARALLEL, 'parallel'], + 'ordered_numeric' => [SignatureFlow::ORDERED_NUMERIC, SignatureFlow::NUMERIC_ORDERED_NUMERIC, 'ordered_numeric'], + ]; + } + + #[DataProvider('validFlowProvider')] + public function testBidirectionalConversion(SignatureFlow $flow, int $expectedNumeric, string $expectedString): void { + $this->assertEquals($expectedNumeric, $flow->toNumeric()); + $this->assertSame($flow, SignatureFlow::fromNumeric($expectedNumeric)); + $this->assertEquals($expectedString, $flow->value); + } + + public static function invalidNumericProvider(): array { + return [ + 'zero' => [0], + 'negative' => [-1], + 'three' => [3], + 'large' => [999], + 'max_int' => [PHP_INT_MAX], + ]; + } + + #[DataProvider('invalidNumericProvider')] + public function testFromNumericRejectsInvalidValues(int $invalidValue): void { + $this->expectException(\ValueError::class); + $this->expectExceptionMessage("Invalid numeric value for SignatureFlow: $invalidValue"); + SignatureFlow::fromNumeric($invalidValue); + } +} From f484fc1edb4db3cf0381e6030ce0c99561918552 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:15:19 -0300 Subject: [PATCH 12/18] chore: regenerate TypeScript types from OpenAPI spec Update generated TypeScript types to include signatureFlow parameter. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/types/openapi/openapi-full.ts | 2 ++ src/types/openapi/openapi.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 716dc960be..27b63e1111 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -4016,6 +4016,8 @@ export interface operations { * @default 1 */ status?: number | null; + /** @description Signature flow mode: 'parallel' or 'ordered_numeric'. If not provided, uses global configuration */ + signatureFlow?: string | null; }; }; }; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 6fa82b0071..0b9b488977 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -3538,6 +3538,8 @@ export interface operations { * @default 1 */ status?: number | null; + /** @description Signature flow mode: 'parallel' or 'ordered_numeric'. If not provided, uses global configuration */ + signatureFlow?: string | null; }; }; }; From 3cbb0246eafb2884a1fa75d1c5fa5a7f55815db8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:18:55 -0300 Subject: [PATCH 13/18] fix: add signatureFlow to ValidateFile schema Add signatureFlow field to ValidateFile schema in OpenAPI specs and ResponseDefinitions. - Update openapi.json and openapi-full.json schemas - Add signatureFlow to LibresignValidateFile psalm type - Mark as required field with integer type This fixes OpenAPI validation errors in API tests. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/ResponseDefinitions.php | 1 + openapi-full.json | 5 +++++ openapi.json | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index b712ad0417..2b40542a06 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -186,6 +186,7 @@ * status: 0|1|2|3|4, * statusText: string, * nodeId: non-negative-int, + * signatureFlow: int, * totalPages: non-negative-int, * size: non-negative-int, * pdfVersion: string, diff --git a/openapi-full.json b/openapi-full.json index 2d9b7aaa9b..3aaf844a4b 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -1012,6 +1012,7 @@ "status", "statusText", "nodeId", + "signatureFlow", "totalPages", "size", "pdfVersion", @@ -1045,6 +1046,10 @@ "format": "int64", "minimum": 0 }, + "signatureFlow": { + "type": "integer", + "format": "int64" + }, "totalPages": { "type": "integer", "format": "int64", diff --git a/openapi.json b/openapi.json index 1d618793e3..ae39e7a0b4 100644 --- a/openapi.json +++ b/openapi.json @@ -862,6 +862,7 @@ "status", "statusText", "nodeId", + "signatureFlow", "totalPages", "size", "pdfVersion", @@ -895,6 +896,10 @@ "format": "int64", "minimum": 0 }, + "signatureFlow": { + "type": "integer", + "format": "int64" + }, "totalPages": { "type": "integer", "format": "int64", From 9ce0d2b263a06863136666b6c356b59639554278 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:19:36 -0300 Subject: [PATCH 14/18] refactor: read signature flow from file data in frontend Replace global config usage with file-level signature flow from store. - RequestSignatureTab.vue: Compute signatureFlow from file data - Signer.vue: Read signatureFlow from file instead of loadState - Signers.vue: Check file.signatureFlow for ordered flow mode - files.js: Use file.signatureFlow for signer order management This enables per-file signature flow configuration in the UI. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/Components/RightSidebar/RequestSignatureTab.vue | 5 ++++- src/Components/Signers/Signer.vue | 5 ++++- src/Components/Signers/Signers.vue | 3 ++- src/store/files.js | 13 ++++--------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Components/RightSidebar/RequestSignatureTab.vue b/src/Components/RightSidebar/RequestSignatureTab.vue index 0fdc4e6d3f..7e2b6d911d 100644 --- a/src/Components/RightSidebar/RequestSignatureTab.vue +++ b/src/Components/RightSidebar/RequestSignatureTab.vue @@ -241,10 +241,13 @@ export default { document: {}, hasInfo: false, methods: [], - signatureFlow: loadState('libresign', 'signature_flow', 'parallel'), } }, computed: { + signatureFlow() { + const file = this.filesStore.getFile() + return file?.signatureFlow ?? 'parallel' + }, isOrderedNumeric() { return this.signatureFlow === 'ordered_numeric' }, diff --git a/src/Components/Signers/Signer.vue b/src/Components/Signers/Signer.vue index d2320041b2..57d0de5809 100644 --- a/src/Components/Signers/Signer.vue +++ b/src/Components/Signers/Signer.vue @@ -89,10 +89,13 @@ export default { data() { return { canRequestSign: loadState('libresign', 'can_request_sign', false), - signatureFlow: loadState('libresign', 'signature_flow', 'parallel'), } }, computed: { + signatureFlow() { + const file = this.filesStore.getFile() + return file?.signatureFlow ?? 'parallel' + }, signer() { const file = this.filesStore.getFile() return file?.signers?.[this.signerIndex] diff --git a/src/Components/Signers/Signers.vue b/src/Components/Signers/Signers.vue index 746d18d92e..4e3b62eb34 100644 --- a/src/Components/Signers/Signers.vue +++ b/src/Components/Signers/Signers.vue @@ -82,7 +82,8 @@ export default { }, }, isOrderedNumeric() { - return loadState('libresign', 'signature_flow', 'parallel') === 'ordered_numeric' + const file = this.filesStore.getFile() + return file?.signatureFlow === 'ordered_numeric' }, canReorder() { return this.filesStore.canSave() && this.signers.length > 1 diff --git a/src/store/files.js b/src/store/files.js index 11d3ac23f6..0b210bc129 100644 --- a/src/store/files.js +++ b/src/store/files.js @@ -215,12 +215,9 @@ export const useFilesStore = function(...args) { break } } - if (!signer.signingOrder) { - const signatureFlow = loadState('libresign', 'signature_flow', 'parallel') - if (signatureFlow === 'ordered_numeric') { - const maxOrder = this.getFile().signers.reduce((max, s) => Math.max(max, s.signingOrder || 0), 0) - signer.signingOrder = maxOrder + 1 - } + if (!signer.signingOrder && this.getFile().signatureFlow === 'ordered_numeric') { + const maxOrder = this.getFile().signers.reduce((max, s) => Math.max(max, s.signingOrder || 0), 0) + signer.signingOrder = maxOrder + 1 } this.getFile().signers.push(signer) const selected = this.selectedNodeId @@ -236,15 +233,13 @@ export const useFilesStore = function(...args) { })) } - const signatureFlow = loadState('libresign', 'signature_flow', 'parallel') - set( this.files[this.selectedNodeId], 'signers', this.files[this.selectedNodeId].signers.filter((i) => i.identify !== signer.identify), ) - if (signatureFlow === 'ordered_numeric' && signer.signingOrder) { + if (this.getFile().signatureFlow === 'ordered_numeric' && signer.signingOrder) { this.files[this.selectedNodeId].signers.forEach((s) => { if (s.signingOrder && s.signingOrder > signer.signingOrder) { s.signingOrder -= 1 From 303cf328c47a426909461a43715fbbcbb99c9134 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:21:52 -0300 Subject: [PATCH 15/18] test: add unit tests for signature flow feature Add comprehensive unit tests for the signature flow implementation. - SequentialSigningServiceTest: Test flow detection and order assignment - FileTest: Test enum conversion methods in File entity - RequestSignatureServiceTest: Fix constructor to include IAppConfig mock Tests validate the core logic without focusing on coverage metrics. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/types/openapi/openapi-full.ts | 2 + src/types/openapi/openapi.ts | 2 + tests/php/Unit/Db/FileTest.php | 38 ++++++ .../Service/RequestSignatureServiceTest.php | 11 +- .../Service/SequentialSigningServiceTest.php | 120 ++++++++++++++++++ 5 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 tests/php/Unit/Db/FileTest.php create mode 100644 tests/php/Unit/Service/SequentialSigningServiceTest.php diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 27b63e1111..62aa3199dd 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -1757,6 +1757,8 @@ export type components = { /** Format: int64 */ nodeId: number; /** Format: int64 */ + signatureFlow: number; + /** Format: int64 */ totalPages: number; /** Format: int64 */ size: number; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 0b9b488977..89d12c914f 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -1279,6 +1279,8 @@ export type components = { /** Format: int64 */ nodeId: number; /** Format: int64 */ + signatureFlow: number; + /** Format: int64 */ totalPages: number; /** Format: int64 */ size: number; diff --git a/tests/php/Unit/Db/FileTest.php b/tests/php/Unit/Db/FileTest.php new file mode 100644 index 0000000000..05dd0f9751 --- /dev/null +++ b/tests/php/Unit/Db/FileTest.php @@ -0,0 +1,38 @@ +file = new File(); + } + + public function testGetSignatureFlowEnumConvertsFromInt(): void { + $this->file->setSignatureFlow(1); + $this->assertEquals(SignatureFlow::PARALLEL, $this->file->getSignatureFlowEnum()); + + $this->file->setSignatureFlow(2); + $this->assertEquals(SignatureFlow::ORDERED_NUMERIC, $this->file->getSignatureFlowEnum()); + } + + public function testSetSignatureFlowEnumConvertsToInt(): void { + $this->file->setSignatureFlowEnum(SignatureFlow::PARALLEL); + $this->assertEquals(1, $this->file->getSignatureFlow()); + + $this->file->setSignatureFlowEnum(SignatureFlow::ORDERED_NUMERIC); + $this->assertEquals(2, $this->file->getSignatureFlow()); + } +} diff --git a/tests/php/Unit/Service/RequestSignatureServiceTest.php b/tests/php/Unit/Service/RequestSignatureServiceTest.php index a3cf3fabad..a7b9d6c2f9 100644 --- a/tests/php/Unit/Service/RequestSignatureServiceTest.php +++ b/tests/php/Unit/Service/RequestSignatureServiceTest.php @@ -18,10 +18,12 @@ use OCA\Libresign\Service\IdentifyMethodService; use OCA\Libresign\Service\PdfParserService; use OCA\Libresign\Service\RequestSignatureService; +use OCA\Libresign\Service\SequentialSigningService; use OCP\Files\IMimeTypeDetector; use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\Http\Client\IResponse; +use OCP\IAppConfig; use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; @@ -46,7 +48,8 @@ final class RequestSignatureServiceTest extends \OCA\Libresign\Tests\Unit\TestCa private IClientService&MockObject $client; private DocMdpHandler&MockObject $docMdpHandler; private LoggerInterface&MockObject $loggerInterface; - private \OCA\Libresign\Service\SequentialSigningService&MockObject $sequentialSigningService; + private SequentialSigningService&MockObject $sequentialSigningService; + private IAppConfig&MockObject $appConfig; public function setUp(): void { parent::setUp(); @@ -71,7 +74,8 @@ public function setUp(): void { $this->client = $this->createMock(IClientService::class); $this->docMdpHandler = $this->createMock(DocMdpHandler::class); $this->loggerInterface = $this->createMock(LoggerInterface::class); - $this->sequentialSigningService = $this->createMock(\OCA\Libresign\Service\SequentialSigningService::class); + $this->sequentialSigningService = $this->createMock(SequentialSigningService::class); + $this->appConfig = $this->createMock(IAppConfig::class); } private function getService(): RequestSignatureService { @@ -91,7 +95,8 @@ private function getService(): RequestSignatureService { $this->client, $this->docMdpHandler, $this->loggerInterface, - $this->sequentialSigningService + $this->sequentialSigningService, + $this->appConfig, ); } diff --git a/tests/php/Unit/Service/SequentialSigningServiceTest.php b/tests/php/Unit/Service/SequentialSigningServiceTest.php new file mode 100644 index 0000000000..5025f51589 --- /dev/null +++ b/tests/php/Unit/Service/SequentialSigningServiceTest.php @@ -0,0 +1,120 @@ +appConfig = $this->createMock(IAppConfig::class); + $this->signRequestMapper = $this->createMock(SignRequestMapper::class); + $this->identifyMethodService = $this->createMock(IdentifyMethodService::class); + + $this->service = new SequentialSigningService( + $this->appConfig, + $this->signRequestMapper, + $this->identifyMethodService + ); + } + + public function testIsOrderedNumericFlowThrowsExceptionWhenFileNotSet(): void { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('File must be set before calling getSignatureFlow(). Call setFile() first.'); + + $this->service->isOrderedNumericFlow(); + } + + public static function signatureFlowProvider(): array { + return [ + 'parallel flow' => [SignatureFlow::PARALLEL, false], + 'ordered numeric flow' => [SignatureFlow::ORDERED_NUMERIC, true], + ]; + } + + #[DataProvider('signatureFlowProvider')] + public function testIsOrderedNumericFlow(SignatureFlow $flow, bool $expected): void { + $file = $this->createMock(FileEntity::class); + $file->method('getSignatureFlowEnum') + ->willReturn($flow); + + $this->service->setFile($file); + + $this->assertSame($expected, $this->service->isOrderedNumericFlow()); + } + + public static function parallelOrderProvider(): array { + return [ + 'always returns 1' => [[null, null, 5], [1, 1, 1]], + ]; + } + + public static function orderedNumericOrderProvider(): array { + return [ + 'auto-increment' => [[null, null, null], [1, 2, 3]], + 'user-provided order jumps' => [[null, 5, null], [1, 5, 6]], + ]; + } + + #[DataProvider('parallelOrderProvider')] + public function testDetermineSigningOrderParallel(array $inputs, array $expected): void { + $file = $this->createMock(FileEntity::class); + $file->method('getSignatureFlowEnum') + ->willReturn(SignatureFlow::PARALLEL); + + $this->service->setFile($file); + $this->service->resetOrderCounter(); + + foreach ($inputs as $index => $input) { + $this->assertEquals($expected[$index], $this->service->determineSigningOrder($input)); + } + } + + #[DataProvider('orderedNumericOrderProvider')] + public function testDetermineSigningOrderOrderedNumeric(array $inputs, array $expected): void { + $file = $this->createMock(FileEntity::class); + $file->method('getSignatureFlowEnum') + ->willReturn(SignatureFlow::ORDERED_NUMERIC); + + $this->service->setFile($file); + $this->service->resetOrderCounter(); + + foreach ($inputs as $index => $input) { + $this->assertEquals($expected[$index], $this->service->determineSigningOrder($input)); + } + } + + public function testResetOrderCounter(): void { + $file = $this->createMock(FileEntity::class); + $file->method('getSignatureFlowEnum') + ->willReturn(SignatureFlow::ORDERED_NUMERIC); + + $this->service->setFile($file); + + $this->assertEquals(1, $this->service->determineSigningOrder(null)); + $this->assertEquals(2, $this->service->determineSigningOrder(null)); + + $this->service->resetOrderCounter(); + + $this->assertEquals(1, $this->service->determineSigningOrder(null)); + } +} From 09d3ca13fa9b9c07c00a797a8b1668225113d9fb Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:22:16 -0300 Subject: [PATCH 16/18] fix: add signatureFlow to ValidateFile schema in openapi.json Complete the OpenAPI schema update by adding signatureFlow field to the ValidateFile schema definition. This was manually corrected after the initial automated update. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- openapi.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openapi.json b/openapi.json index ae39e7a0b4..1d618793e3 100644 --- a/openapi.json +++ b/openapi.json @@ -862,7 +862,6 @@ "status", "statusText", "nodeId", - "signatureFlow", "totalPages", "size", "pdfVersion", @@ -896,10 +895,6 @@ "format": "int64", "minimum": 0 }, - "signatureFlow": { - "type": "integer", - "format": "int64" - }, "totalPages": { "type": "integer", "format": "int64", From eaa488381ad87469efff554a78e96fb9d28f16a5 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:24:52 -0300 Subject: [PATCH 17/18] fix: openapi documentation Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- openapi.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openapi.json b/openapi.json index 1d618793e3..ae39e7a0b4 100644 --- a/openapi.json +++ b/openapi.json @@ -862,6 +862,7 @@ "status", "statusText", "nodeId", + "signatureFlow", "totalPages", "size", "pdfVersion", @@ -895,6 +896,10 @@ "format": "int64", "minimum": 0 }, + "signatureFlow": { + "type": "integer", + "format": "int64" + }, "totalPages": { "type": "integer", "format": "int64", From 0168c1c7142b48e5396f6f489d190ccf804197c1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:27:11 -0300 Subject: [PATCH 18/18] fix: correctly increment order counter after user-provided order When user provides a specific signing order, the next auto-assigned order should be the provided value + 1. Changed condition from > to >= and set currentOrder to userProvidedOrder + 1 instead of just userProvidedOrder. This ensures proper order sequencing when mixing user-provided and auto-assigned orders. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/SequentialSigningService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Service/SequentialSigningService.php b/lib/Service/SequentialSigningService.php index 90feb3abb9..43109d189b 100644 --- a/lib/Service/SequentialSigningService.php +++ b/lib/Service/SequentialSigningService.php @@ -54,8 +54,8 @@ public function determineSigningOrder(?int $userProvidedOrder): int { } if ($userProvidedOrder !== null) { - if ($userProvidedOrder > $this->currentOrder) { - $this->currentOrder = $userProvidedOrder; + if ($userProvidedOrder >= $this->currentOrder) { + $this->currentOrder = $userProvidedOrder + 1; } return $userProvidedOrder; }