/ app / Http / Services / DocumentService.php
DocumentService.php
   1  <?php
   2  
   3  namespace App\Http\Services;
   4  
   5  use App\Helpers\DocGen\DocumentDownloader;
   6  use App\Models\Review;
   7  use Exception;
   8  use Illuminate\Support\Str;
   9  use Throwable;
  10  use App\Models\Dossier;
  11  use App\Models\Document;
  12  use App\Models\Revision;
  13  use App\Models\Template;
  14  use App\Enums\UserTypeEnum;
  15  use App\Events\DocumentGenerated;
  16  use App\Helpers\DocxToPdfConverter;
  17  use App\Models\DocumentType;
  18  use App\Models\Folder;
  19  use App\Models\Notification;
  20  use App\Models\Role;
  21  use App\Models\User;
  22  use App\Models\Validation;
  23  use App\Notifications\DocumentForApproval;
  24  use App\Notifications\DocumentForReview;
  25  use Illuminate\Http\Response;
  26  use Illuminate\Support\Facades\DB;
  27  use Illuminate\Support\Facades\Log;
  28  use Illuminate\Support\Facades\Auth;
  29  use Illuminate\Database\Eloquent\Model;
  30  use Illuminate\Support\Facades\Storage;
  31  use Illuminate\Database\Eloquent\Builder;
  32  use Illuminate\Http\Request;
  33  use Illuminate\Support\Collection;
  34  use InvalidArgumentException;
  35  use Livewire\TemporaryUploadedFile;
  36  
  37  class DocumentService
  38  {
  39      public static function generateFile(
  40          DocumentType $type,
  41          Revision     $revision,
  42          ?int         $notificatedReportId = null,
  43          ?int         $certificateId = null,
  44          ?bool        $convertToPdf = true,
  45          array        $excludeTokens = []
  46      ) {
  47          $docGenName = $revision->template->getClassDocGen();
  48          $document = $revision->document;
  49  
  50          if ($type->code === 'INF') {
  51              if ($document->original->related_type == Validation::class) {
  52                  $document = $document->revisions()->where('related_type', Validation::class)->latest()->first()?->related;
  53              }
  54          }
  55  
  56          if ($type->code === 'NOT') {
  57              $document = Notification::create([
  58                  'dossier_id' => $revision->document->dossier->id,
  59                  'revision_id' => $revision->id,
  60                  'notificated_report_id' => $notificatedReportId
  61              ]);
  62              $revision->update(['notification_id' => $notificatedReportId]);
  63          }
  64  
  65          $docGen = new $docGenName($document, $certificateId);
  66          return $docGen->save($revision->path, $convertToPdf, excludeTokens: $excludeTokens);
  67      }
  68  
  69      private static function getGenerationData(bool $new, ?Document $document = null, ?Template $template = null, ?Dossier $dossier = null, ?DocumentType $type = null, ?bool $convertToPdf = true): array
  70      {
  71          $explodedTemplateName = !$new
  72              ? explode('.', $document->head->template->filename)
  73              : explode('.', $template->filename);
  74  
  75          $explodedFilename = explode('-v', $explodedTemplateName[0]);
  76  
  77          $version = !$new ? $document->head->version + 1 : 1;
  78          $name = !$new ? $explodedFilename[0] . '-v' . $version : $template->name;
  79          $filename = !$new ? $name . '.' . $explodedTemplateName[1] : $template->filename;
  80  
  81          if (!$document) {
  82              $number = Document::where('type_id', $type->id)->withTrashed()->max('number') + 1;
  83          }
  84  
  85          $path = 'tmp/' . uniqid() . '/' . $filename;
  86          if (Str::endsWith($path, '.docx') && $convertToPdf) {
  87              //we are forcing pdf conversion
  88              $path = substr($path, 0, -4) . 'pdf';
  89          }
  90  
  91          return [
  92              'version' => $version,
  93              'name' => $name . '.' . DocumentService::getExtensions($path),
  94              'path' => $path
  95          ];
  96      }
  97  
  98      public static function generateRevision(Document $document, array $data, int $templateId, ?bool $is_draft = null): Revision
  99      {
 100          $revision = new Revision();
 101  
 102          $revision->document_id = $document->id;
 103          $revision->template_id = $templateId;
 104          $revision->created_by = Auth::id() ?? 1;
 105          $revision->path = $data['path'];
 106          $revision->name = $data['name'];
 107          $revision->version = $data['version'];
 108          $revision->description = $data['description'];
 109          $revision->is_draft = $is_draft ?? false;
 110  
 111          $revision->save();
 112  
 113          $document->update([
 114              'head_id' => $revision->id
 115          ]);
 116  
 117          return $revision;
 118      }
 119  
 120      private static function getNewRevisionPath(Revision $revision): string
 121      {
 122          $extension = pathinfo($revision->path, PATHINFO_EXTENSION);
 123          $templateName = $revision->template->name;
 124          $newPath = 'tmp/' . uniqid() . '/' . $templateName . '.' . $extension;
 125          return $newPath;
 126      }
 127  
 128      public static function createRevision(Document $document): Revision
 129      {
 130          $version = upgrade_version($document->head->version);
 131          $revision = $document->revisions()->create([
 132              'document_id' => $document->id,
 133              'created_by' => Auth::id(),
 134              'template_id' => $document->head->template_id,
 135              'from' => 'internal',
 136              'name' => $document->head->name,
 137              'path' => self::getNewRevisionPath($document->head),
 138              'hash' => '',
 139              'size' => 0,
 140              'version' => $version,
 141              'previous_id' => $document->head->id,
 142          ]);
 143  
 144          if ($document->head->related) {
 145              $revision->related()->associate($document->head->related);
 146              $revision->save();
 147          }
 148  
 149          $document->update([
 150              'head_id' => $revision->id
 151          ]);
 152  
 153          $document->fresh();
 154  
 155          return $revision;
 156      }
 157  
 158      public static function makeReviewed(Document $document): Revision
 159      {
 160          $revision = self::createRevision($document);
 161          $revision->makeReviewed();
 162          self::generateFile($document->type, $revision);
 163  
 164          $revision->update([
 165              'hash' => md5_file(Storage::path($revision->path)),
 166              'size' => Storage::fileSize($revision->path),
 167              'is_pending' => false,
 168          ]);
 169          $revision->moveToFilesystem();
 170  
 171          return $revision;
 172      }
 173  
 174      public static function makeApproved(Document $document): Revision
 175      {
 176          $newRevision = self::createRevision($document);
 177          $newRevision->makeApproved();
 178          $document->refresh();
 179  
 180          self::generateFile($document->type, $newRevision, convertToPdf: true);
 181  
 182          $newRevision->update([
 183              'hash' => md5_file(Storage::path($newRevision->path)),
 184              'size' => Storage::fileSize($newRevision->path),
 185              'is_pending' => false,
 186          ]);
 187          $newRevision->moveToFilesystem();
 188          $document->refresh();
 189  
 190          $revision = self::sign($document);
 191          $revision->makeApproved();
 192  
 193          return $revision;
 194      }
 195  
 196      public static function removeDraft(Document $document)
 197      {
 198          $revision = self::createRevision($document);
 199          self::generateFile($document->type, $revision);
 200  
 201  
 202          $revision->update([
 203              'hash' => md5_file(Storage::path($revision->path)),
 204              'size' => Storage::fileSize($revision->path),
 205              'is_draft' => false,
 206              'is_pending' => false,
 207          ]);
 208          $document->refresh();
 209          event(new DocumentGenerated($document));
 210          $revision->moveToFilesystem();
 211          return $revision;
 212      }
 213  
 214      public static function sign(Document $document): Revision
 215      {
 216          $gpgService = new GpgService();
 217  
 218          $toSign = $document->head;
 219  
 220          $revision = self::createRevision($document);
 221          $user = auth()->user();
 222          $keyPassPhrase = $user->activeKey->key_passphrase;
 223  
 224          Storage::makeDirectory(dirname($revision->path));
 225          $path = $gpgService->signFile($user, $keyPassPhrase, $toSign, $revision->path);
 226  
 227          $revision->update([
 228              'path' => $path,
 229              'name' => basename($path),
 230              'hash' => md5_file(Storage::path($path)),
 231              'size' => Storage::fileSize($path),
 232              'is_pending' => false,
 233          ]);
 234  
 235          $document->update([
 236              'head_id' => $revision->id
 237          ]);
 238  
 239          $revision->moveToFilesystem();
 240  
 241          return $revision;
 242      }
 243  
 244      /**
 245       * @throws Exception
 246       */
 247      public static function addNewRevision(int $documentId, ?string $description = '', bool $isDraft = false)
 248      {
 249          $document = Document::findOrFail($documentId);
 250          $type = $document->type;
 251  
 252          if ($type->code === 'EXT' || $type->code === 'EML') {
 253              throw new Exception(__('documents.notifications.update.error.message'));
 254          }
 255  
 256          $data = self::getGenerationData(false, $document, convertToPdf: false);
 257  
 258          try {
 259              DB::beginTransaction();
 260  
 261              $data['description'] = $description;
 262  
 263              $revision = self::generateRevision($document, $data, $document->head->template_id, $isDraft);
 264              $path = $revision->path;
 265  
 266              $notificatedReportId = $document->head->notifications->first()
 267                  ? $document->head->notifications->first()->notificated_report_id
 268                  : null;
 269  
 270              $document->update([
 271                  'head_id' => $revision->id
 272              ]);
 273  
 274              self::generateFile($type, $revision, $notificatedReportId, convertToPdf: false);
 275  
 276              $revision->update([
 277                  'hash' => md5_file(Storage::path($path)),
 278                  'size' => Storage::size($path),
 279              ]);
 280  
 281              DB::commit();
 282              return true;
 283          } catch (Exception $e) {
 284              DB::rollBack();
 285              Storage::delete($data['path']);
 286              log_exception($e);
 287  
 288              throw $e;
 289          }
 290      }
 291  
 292      public static function createWithTemplate(
 293          array $data,
 294          ?int  $userId = null,
 295          ?int  $notificatedReportId = null,
 296          ?int  $certificateId = null,
 297          ?bool $convertToPdf = true,
 298          array $excludeTokens = []
 299      ): Document {
 300          $type = DocumentType::findOrFail($data['type_id']);
 301          $template = Template::findOrFail($data['template_id']);
 302          $dossier = Dossier::findOrFail($data['dossier_id']);
 303  
 304          if ($type->code === 'EXT' || $type->code === 'EML') {
 305              throw new Exception(__('documents.notifications.create.error.message'));
 306          }
 307  
 308          $revisionData = self::getGenerationData(true, null, $template, $dossier, $type, $convertToPdf);
 309  
 310          try {
 311              DB::beginTransaction();
 312  
 313              $document = Document::create([
 314                  'type_id' => $type->id,
 315                  'dossier_id' => $dossier->id,
 316                  'created_by' => $userId ?? Auth::id(),
 317                  'from' => 'internal',
 318                  'meet_id' => $data['meet_id'],
 319                  'template_id' => $template->id,
 320              ]);
 321  
 322              $revisionData['description'] = $data['description'] ?? '';
 323  
 324              $revision = self::generateRevision($document, $revisionData, $template->id, $data['is_draft'] ?? false);
 325  
 326              $document->original_id = $revision->id;
 327              $document->save();
 328  
 329              self::generateFile($type, $revision, $notificatedReportId, $certificateId, $convertToPdf, $excludeTokens);
 330              $revision->update([
 331                  'hash' => md5_file(Storage::path($revision->path)),
 332                  'size' => Storage::size($revision->path)
 333              ]);
 334  
 335              DB::commit();
 336              return $document;
 337          } catch (Exception $e) {
 338              DB::rollBack();
 339              Storage::delete($revisionData['path']);
 340  
 341              throw $e;
 342          }
 343      }
 344  
 345      public static function searchRevisionInDocument(int $documentId, string $searchString, string $sortField, string $sortDirection)
 346      {
 347          $documentQuery = Revision::where('document_id', $documentId)
 348              ->where('name', 'like', '%' . $searchString . '%');
 349  
 350          return $sortField && $sortDirection
 351              ? $documentQuery->orderBy($sortField, $sortDirection)
 352              : $documentQuery;
 353      }
 354  
 355      public static function searchInDossier(int $dossierId, string $searchString, string $sortField, string $sortDirection, string $folderId = null)
 356      {
 357          $documentQuery = Document::where('dossier_id', $dossierId)
 358              ->where('name', 'like', '%' . $searchString . '%')
 359              ->when($folderId, function ($query) use ($folderId) {
 360                  return $query->where('folder_id', $folderId);
 361              });
 362  
 363          return $sortField && $sortDirection
 364              ? $documentQuery->orderBy($sortField, $sortDirection)
 365              : $documentQuery;
 366      }
 367  
 368      public static function deleteRevision(int $id)
 369      {
 370          return Revision::findOrFail($id)->delete();
 371      }
 372  
 373      /**
 374       * Display a listing of the resource.
 375       */
 376      public static function find(int $id): Document|Builder|Model
 377      {
 378          return Document::where('id', $id)->first();
 379      }
 380  
 381      /**
 382       * Search resources
 383       */
 384      public static function search(bool $isInternal, string $searchString, string $sortField, string $sortDirection, array|null $filters, ?User $authUser = null): Document|Builder
 385      {
 386          $query = Document::filterByUser($authUser);
 387  
 388          $query->whereHas('type', function ($q) use ($isInternal) {
 389              $q->where('internal', $isInternal);
 390          });
 391  
 392          $query->when($filters && $filters['withTrashed'], fn($query) => $query->withTrashed());
 393  
 394          $query
 395              ->where(function ($query) use ($searchString) {
 396                  $query
 397                      ->where('name', 'like', '%' . $searchString . '%')
 398                      ->orWhere('number', 'like', '%' . $searchString . '%')
 399                      ->orWhere('description', 'like', '%' . $searchString . '%')
 400                      ->orWhere('original_name', 'like', '%' . $searchString . '%')
 401                      ->orWhereHas('head', function ($query) use ($searchString) {
 402                          $query->where('name', 'like', '%' . $searchString . '%');
 403                      });
 404              })
 405              ->when($filters && !empty($filters['types']), fn($query) => $query->whereIn('type_id', $filters['types']))
 406              ->when($filters && $filters['created_at_since'], fn($query) => $query->where('created_at', '>=', $filters['created_at_since']))
 407              ->when($filters && $filters['created_at_to'], fn($query) => $query->where('created_at', '<=', $filters['created_at_to']))
 408              ->when($filters && $filters['user_by'], function ($query) use ($filters) {
 409                  /**Search by created_by, reviewed_by or approved_by */
 410                  $query->where(function ($query) use ($filters) {
 411                      $query->whereHas('createdBy', function ($query) use ($filters) {
 412                          $query->where('name', 'like', '%' . $filters['user_by'] . '%');
 413                      })->orWhereHas('head', function ($query) use ($filters) {
 414                          $query->whereHas('reviewedBy', function ($query) use ($filters) {
 415                              $query->where('name', 'like', '%' . $filters['user_by'] . '%');
 416                          })->orWhereHas('approvedBy', function ($query) use ($filters) {
 417                              $query->where('name', 'like', '%' . $filters['user_by'] . '%');
 418                          });
 419                      });
 420                  });
 421              })
 422              ->when($filters && $filters['reviewed_at_since'], function ($query) use ($filters) {
 423                  $query->whereHas('head', function ($query) use ($filters) {
 424                      $query->where('reviewed_at', '>=', $filters['reviewed_at_since']);
 425                  });
 426              })
 427              ->when($filters && $filters['reviewed_at_to'], function ($query) use ($filters) {
 428                  $query->whereHas('head', function ($query) use ($filters) {
 429                      $query->where('reviewed_at', '<=', $filters['reviewed_at_to']);
 430                  });
 431              })
 432              ->when($filters && $filters['approved_at_since'], function ($query) use ($filters) {
 433                  $query->whereHas('head', function ($query) use ($filters) {
 434                      $query->where('approved_at', '>=', $filters['approved_at_since']);
 435                  });
 436              })
 437              ->when($filters && $filters['approved_at_to'], function ($query) use ($filters) {
 438                  $query->whereHas('head', function ($query) use ($filters) {
 439                      $query->where('approved_at', '<=', $filters['approved_at_to']);
 440                  });
 441              });
 442  
 443  
 444          if ($sortField && $sortDirection) {
 445              $query->orderBy($sortField, $sortDirection);
 446          }
 447  
 448          return $query;
 449      }
 450  
 451      public static function getRevisions(array|null $filters, Document $document)
 452      {
 453          return $document->revisions()->latest()
 454              ->when($filters, fn($query) => $query->where(function ($query) use ($filters) {
 455                  $query->where('name', 'like', '%' . $filters['search'] . '%')
 456                      ->orWhere('version', 'like', '%' . $filters['search'] . '%')
 457                      ->orWhere('description', 'like', '%' . $filters['search'] . '%');
 458              })
 459                  ->when($filters['created_at_since'], fn($query) => $query->where('created_at', '>=', $filters['created_at_since']))
 460                  ->when($filters['created_at_to'], fn($query) => $query->where('created_at', '<=', $filters['created_at_to']))
 461                  ->when($filters['user_by'], fn($query) => $query->where(function ($query) use ($filters) {
 462                      $query->whereHas('createdBy', function ($query) use ($filters) {
 463                          $query->where('name', 'like', '%' . $filters['user_by'] . '%');
 464                      })->orWhereHas('reviewedBy', function ($query) use ($filters) {
 465                          $query->where('name', 'like', '%' . $filters['user_by'] . '%');
 466                      })->orWhereHas('approvedBy', function ($query) use ($filters) {
 467                          $query->where('name', 'like', '%' . $filters['user_by'] . '%');
 468                      });
 469                  }))
 470                  ->when($filters['reviewed_at_since'], function ($query) use ($filters) {
 471                      $query->where('reviewed_at', '>=', $filters['reviewed_at_since']);
 472                  })
 473                  ->when($filters['reviewed_at_to'], function ($query) use ($filters) {
 474                      $query->where('reviewed_at', '<=', $filters['reviewed_at_to']);
 475                  })
 476                  ->when($filters['approved_at_since'], function ($query) use ($filters) {
 477                      $query->where('approved_at', '>=', $filters['approved_at_since']);
 478                  })
 479                  ->when($filters['approved_at_to'], function ($query) use ($filters) {
 480                      $query->where('approved_at', '<=', $filters['approved_at_to']);
 481                  })
 482                  ->when($filters['withTrashed'], fn($query) => $query->withTrashed()))
 483              ->orderBy('id', 'desc');
 484      }
 485  
 486      /**
 487       * Create a resource
 488       *
 489       * @throws Throwable
 490       */
 491      public static function create(array $data): Document|Exception
 492      {
 493          try {
 494              DB::beginTransaction();
 495  
 496              if ($data['isInternal']) {
 497                  $data['from'] = 'internal';
 498              } else {
 499                  $data['from'] = 'external';
 500              }
 501  
 502              $doc = Document::create($data);
 503              $doc->commit($data);
 504              $doc->original_id = $doc->head_id;
 505              $doc->save();
 506  
 507              DB::commit();
 508  
 509              return $doc;
 510          } catch (Exception $e) {
 511              DB::rollBack();
 512              throw new Exception($e);
 513          }
 514      }
 515  
 516      public static function GenerateCopyAsSignedPdf(Document $doc): Document
 517      {
 518          $path = DocumentDownloader::docxToSignedPdf($doc->head->path);
 519          $name = basename($path);
 520  
 521          return self::createOrVersionate([
 522              'name' => $name,
 523              'revision_name' => $name,
 524              'hash' => md5_file(Storage::path($path)),
 525              'size' => Storage::size($path),
 526              'isInternal' => $doc->isInternal(),
 527              'dossier_id' => $doc->dossier_id,
 528              'folder_id' => $doc->folder_id,
 529              'type_id' => $doc->type_id,
 530              'created_by' => Auth::id(),
 531              'from' => $doc->isInternal() ? 'internal' : 'external',
 532              'path' => $path,
 533              'active' => true,
 534              'description' => '',
 535              'is_template' => $doc->head->is_template
 536          ]);
 537      }
 538  
 539      public static function createOrVersionate(array $data): Document
 540      {
 541          $doc = self::findByDataArray($data);
 542          if ($doc) {
 543              self::addRevision($doc->id, $data['path']);
 544          } else {
 545              $doc = self::create($data);
 546          }
 547  
 548          return $doc->refresh();
 549      }
 550  
 551      /**
 552       * Find a document from the data array used for creation
 553       * @param $data
 554       * @return Document|null
 555       */
 556      public static function findByDataArray($data)
 557      {
 558          return Document::where('dossier_id', $data['dossier_id'])
 559              ->where('folder_id', $data['folder_id'])
 560              ->where('name', $data['name'])
 561              ->first();
 562      }
 563  
 564      public static function addRevision($document_id, $data, $version = null): Revision
 565      {
 566          try {
 567              DB::beginTransaction();
 568              $doc = Document::findOrFail($document_id);
 569              $revision = $doc->commit([
 570                  'path' => $data['path'],
 571                  'revision_name' => $data['name'],
 572                  'version' => $version,
 573                  'from' => $doc->isExternal() ? 'external' : 'internal',
 574                  'related' => $data['related'] ?? $doc->head->related,
 575                  'description' => $data['description'] ?? '',
 576                  'is_template' => $data['is_template'] ?? false,
 577              ]);
 578              $revision->moveToFilesystem();
 579              DB::commit();
 580  
 581              return $revision;
 582          } catch (Exception $e) {
 583              DB::rollBack();
 584              log_exception($e);
 585              throw new Exception(__('documents.notifications.update.error.message'));
 586          }
 587      }
 588  
 589      /**
 590       * Update a document description
 591       *
 592       * @throws Throwable
 593       */
 594      public static function update(int $id, string $description): void
 595      {
 596          DB::beginTransaction();
 597  
 598          try {
 599              Document::findOrFail($id)?->head?->update([
 600                  'description' => $description,
 601              ]);
 602  
 603              DB::commit();
 604          } catch (Exception $e) {
 605              DB::rollBack();
 606              throw new Exception(__('documents.notifications.update.error.message'));
 607          }
 608      }
 609  
 610      /**
 611       * Update a revision name and description
 612       *
 613       * @throws Throwable
 614       */
 615      public static function updateRevision(int $id, string $name, ?string $description): void
 616      {
 617          DB::beginTransaction();
 618  
 619          try {
 620              $revision = Revision::findOrFail($id);
 621              $revision->update([
 622                  'name' => $name,
 623                  'description' => $description,
 624              ]);
 625  
 626              DB::commit();
 627          } catch (Exception $e) {
 628              DB::rollBack();
 629              throw new Exception(__('documents.notifications.update.error.message'));
 630          }
 631      }
 632  
 633      /**
 634       * Destroy a resource
 635       *
 636       * @return true
 637       *
 638       * @throws Throwable
 639       */
 640      public static function remove(int $id): bool
 641      {
 642          try {
 643              DB::beginTransaction();
 644              Document::destroy($id);
 645              DB::commit();
 646  
 647              return true;
 648          } catch (Exception $e) {
 649              DB::rollBack();
 650              throw new Exception(__('documents.notifications.remove.error.message'));
 651          }
 652      }
 653  
 654      public static function removeRevision(int $id): bool
 655      {
 656          try {
 657              $revision = Revision::findOrFail($id);
 658  
 659              if ($revision->document->revisions->count() === 1) {
 660                  $documentId = $revision->document->id;
 661                  $dossier = $revision->document?->dossier;
 662                  Document::destroy($documentId);
 663                  //Revision::destroy($id);
 664  
 665                  auth()->user()->hasPermissionTo('can_read_documents') ? redirect()->route('documents.index') : redirect()->route('dossiers.dossier.view', ['dossier' => $dossier]);
 666                  return true;
 667              }
 668  
 669              //Comprobamos que la versión que borramos sea la ultima
 670              // para actualizar y mostrar correctamente los documentos en el index
 671              if ($revision->document->head_id === $id && !is_null($revision->previous_id)) {
 672                  $revision->document()->update(['head_id' => $revision->previous_id]);
 673              }
 674  
 675              Revision::destroy($id);
 676              return true;
 677          } catch (Exception $e) {
 678              logger()->error($e);
 679              throw new Exception(__('documents.notifications.remove.error.message'));
 680          }
 681      }
 682  
 683      public static function hardDeleteRevision(int $id): void
 684      {
 685          try {
 686              DB::beginTransaction();
 687  
 688              $revision = Revision::withTrashed()->find($id);
 689  
 690              Storage::delete($revision->path);
 691              $revision->forceDelete();
 692  
 693              DB::commit();
 694          } catch (Exception $e) {
 695              DB::rollBack();
 696              logger()->error($e);
 697              throw new Exception(__('documents.notifications.remove.error.message'));
 698          }
 699      }
 700  
 701      /**
 702       * Restore a resource
 703       *
 704       * @return true
 705       *
 706       * @throws Throwable
 707       */
 708      public static function restore(int $id): bool
 709      {
 710          try {
 711              DB::beginTransaction();
 712              Document::withTrashed()->find($id)->restore();
 713              //Revision::withTrashed()->where('document_id', $id)->restore();
 714              DB::commit();
 715  
 716              return true;
 717          } catch (Exception $e) {
 718              DB::rollBack();
 719              throw new Exception(__('documents.notifications.remove.error.message'));
 720          }
 721      }
 722  
 723      public static function moveToFolder(int $documentId, int $folderId)
 724      {
 725          try {
 726              DB::beginTransaction();
 727              $document = Document::findOrFail($documentId);
 728              $document->folder_id = $folderId;
 729              $document->save();
 730              DB::commit();
 731              return true;
 732          } catch (Exception $e) {
 733              DB::rollBack();
 734              throw new Exception(__('documents.notifications.update.error.message'));
 735          }
 736      }
 737  
 738      public static function uploadToFolder(TemporaryUploadedFile $file, Folder $folder, string $description, int $typeId): int
 739      {
 740          $name = $file->getClientOriginalName();
 741          $dossier = $folder->dossier;
 742  
 743          $path = $file->storeAs('tmp/' . uniqid() . '/' . $name);
 744          $document = DocumentService::create([
 745              'type_id' => $typeId ? $typeId : DocumentType::where('code', 'EXT')->first()->id,
 746              'file' => $file,
 747              'revision_name' => $name,
 748              'isInternal' => false,
 749              'description' => $description,
 750              'path' => $path,
 751              'dossier_id' => $dossier->id ?? null,
 752              'created_by' => Auth::id(),
 753              'folder_id' => $folder->id,
 754              'hash' => '',
 755              'size' => 0
 756          ]);
 757  
 758          $fixedPath = $document->head->moveToFilesystem();
 759  
 760          $document->head->update([
 761              'hash' => md5_file(Storage::path($fixedPath)),
 762              'size' => Storage::size($fixedPath)
 763          ]);
 764          return $document->id;
 765      }
 766  
 767      private static function createTaskForRoleNeeded(Document $document, ?int $roleId, string $taskTitleFunction)
 768      {
 769          if (!in_array($document->type->code, ['INF', 'NOT', 'ACT', 'RES'])) {
 770              return;
 771          }
 772  
 773          TaskService::$taskTitleFunction($roleId, $document->dossier->stage->id, $document->head->name, $document->head?->id);
 774      }
 775  
 776  
 777      public static function createApprovalTask(Document $document)
 778      {
 779          self::createTaskForRoleNeeded($document, $document->template->approver_role_id, 'createApprovalTask');
 780      }
 781  
 782      /**
 783       * Search templates
 784       * @param string $searchString
 785       * @param string $sortField
 786       * @param string $sortDirection
 787       * @param array|null $filters
 788       * @return Collection<array-key, mixed>
 789       * @throws InvalidArgumentException
 790       */
 791      public static function searchTemplates(string $searchString, string $sortField, string $sortDirection, array|null $filters)
 792      {
 793          $codes = DocumentType::whereIn('code', ['INF', 'NOT', 'ACT'])->pluck('id');
 794  
 795          $templatesQuery = Template::query();
 796  
 797          $templatesQuery
 798              ->where(function ($query) use ($searchString) {
 799                  $query->where('name', 'like', '%' . $searchString . '%');
 800              });
 801  
 802          if ($filters && $filters['reviewer_roles']) {
 803              $templatesQuery->whereIn('reviewer_role_id', $filters['reviewer_roles']);
 804          }
 805  
 806          if ($filters && $filters['approver_roles']) {
 807              $templatesQuery->whereIn('approver_role_id', $filters['approver_roles']);
 808          }
 809  
 810          if ($sortField && $sortDirection) {
 811              $templatesQuery->orderBy($sortField, $sortDirection);
 812          }
 813  
 814          $templates = $templatesQuery->get()->filter(function ($template) {
 815              return Storage::exists($template->path); //Ciertas plantillas no existen
 816          });
 817  
 818          return $templates->map(function ($template) use ($codes) {
 819              if ($codes->contains($template->document_type_id)) {
 820                  $template->isEdit = true;
 821              }
 822              return $template;
 823          });
 824      }
 825  
 826      public static function getDossierDocumentsFilter(int $dossierId, Request $request): Collection
 827      {
 828          $documents = Document::where('dossier_id', $dossierId)
 829              ->with('head', fn($query) => $query->select(['id', 'name', 'description']))
 830              ->orderBy('name')
 831              ->select(['id', 'name', 'head_id'])
 832              ->when(
 833                  $request->exists('search'),
 834                  fn($query) => $query
 835                      ->where(function ($query) use ($request) {
 836                          $query
 837                              ->where('name', 'like', "%{$request->search}%")
 838                              ->orWhereHas(
 839                                  'head',
 840                                  fn($query) => $query->where('name', 'like', "%{$request->search}%")
 841                              );
 842                      })
 843              )->when(
 844                  $request->exists('selected'),
 845                  fn($query) => $query->whereIn('id', $request->selected)
 846              )->get() ?? collect();
 847  
 848          return $documents->map(function ($document) use ($request) {
 849              $description = '<strong>' . $document->head->name . '</strong><br>' . $document->head->description;
 850              return [
 851                  'id' => $request->exists('is_revision') && $request->input('is_revision') ? $document->head_id : $document->id,
 852                  'name' => $document->name,
 853                  'description' => $description,
 854              ];
 855          });
 856      }
 857  
 858      public static function createTemplate($data)
 859      {
 860          try {
 861              DB::beginTransaction();
 862  
 863              Template::create($data);
 864  
 865              DB::commit();
 866          } catch (Exception $e) {
 867              DB::rollBack();
 868              Storage::delete($data['path']);
 869              log_exception($e);
 870              throw new Exception(__('documents.templates.notifications.create.error.message'));
 871          }
 872      }
 873  
 874      public static function uploadDocx(Document $document, $data)
 875      {
 876          try {
 877              DB::beginTransaction();
 878              $version = upgrade_version($document->head->version);
 879              $parts = explode('v', $document->head->name);
 880              unset($parts[count($parts) - 1]);
 881              $parts = implode('v', $parts);
 882              $revisionName = $parts . 'v' . $version . '.' . DocumentService::getExtensions($data['file']->getClientOriginalName());
 883  
 884              $path = 'tmp/' . uniqid() . '/' . $revisionName;
 885              Storage::putFileAs($path, $data['file'], $revisionName);
 886  
 887              $path .= '/' . $revisionName;
 888  
 889              $revision = $document->commit(
 890                  [
 891                      'path' => $path,
 892                      'name' => $revisionName,
 893                      'revision_name' => $revisionName,
 894                      'description' => $data['description'],
 895                      'from' => $document->head->from,
 896                      'created_by' => Auth::id(),
 897                      'template_id' => $document->head->template_id,
 898                      'is_template' => true,
 899                      'version' => $version,
 900                  ]
 901              );
 902  
 903              //Actualizamos el is_peding a false
 904              $revision->update([
 905                  'is_pending' => false,
 906                  'is_draft' => $document->head->is_draft
 907              ]);
 908  
 909              $document->refresh();
 910              $revision->moveToFilesystem();
 911  
 912              DB::commit();
 913          } catch (Exception $e) {
 914              DB::rollBack();
 915              Storage::delete($path);
 916              log_exception($e);
 917              throw new Exception(__('documents.notifications.upload.error.message'));
 918          }
 919      }
 920  
 921      public static function createPDF(Document $document)
 922      {
 923          try {
 924              DB::beginTransaction();
 925  
 926              // Prepare new version data
 927              $path = 'tmp/' . uniqid() . '/' . $document->name . '.pdf';
 928              $filename = explode('.', $document->head->name)[0] . '.pdf';
 929  
 930              // En el caso en el que haya tareas asociadas a la revisión, deberemos asociarlas a la revisión que se va a generar. Esto hará que el flujo de creación y cerrado automático de tareas no se rompa.
 931              $tasks = $document->head->task;
 932  
 933              // Creamos la revision antes de generar el pdf!
 934              $revision = $document->commit(
 935                  [
 936                      'path' => $path,
 937                      'name' => $filename,
 938                      'revision_name' => $filename,
 939                      'description' => $document->head?->description,
 940                      'from' => $document->head->from ?? null,
 941                      'created_by' => Auth::id(),
 942                      'template_id' => $document->head->template_id ?? null,
 943                      'is_template' => false,
 944                      'version' => upgrade_version($document->getLatestRevision()->version),
 945                      'hash' => "",
 946                      'size' => 0,
 947                  ]
 948              );
 949  
 950              $revision->update([
 951                  'is_pending' => false,
 952                  'is_draft' => $document->head->is_draft
 953              ]);
 954  
 955              $revision->task()->saveMany($tasks);
 956  
 957              $document->refresh();
 958  
 959              // Converting to pdf
 960              if ($document->head->template) {
 961                  $path = self::generateFile($document->type, $document->head, convertToPdf: true);
 962              } else {
 963                  $path = DocxToPdfConverter::convert($document->head->previous->path, $document->head->path);
 964              }
 965  
 966              $revision->update([
 967                  'hash' => md5_file(Storage::path($path)),
 968                  'size' => Storage::fileSize($path),
 969              ]);
 970  
 971              $revision->moveToFilesystem();
 972              DB::commit();
 973          } catch (Exception $e) {
 974              DB::rollBack();
 975              Storage::delete($path);
 976              log_exception($e);
 977              throw $e;
 978              throw new Exception(__('documents.notifications.create_pdf.error.message'));
 979          }
 980      }
 981  
 982      public static function getExtensions(string $path)
 983      {
 984          $parts = explode('.', basename($path));
 985          array_shift($parts);
 986          $extensions = implode('.', $parts);
 987  
 988          return $extensions;
 989      }
 990  
 991      public static function getRevisionCorrectPath(Revision $revision)
 992      {
 993          $dossier = $revision->document->dossier;
 994          $root = $dossier
 995              ? 'dossiers/' . $dossier->code
 996              : 'common';
 997  
 998          $documentCode = $revision->document->name; // has zerofill
 999          $version = $revision->version;
1000  
1001          $extensions = self::getExtensions($revision->path);
1002          $filename = $revision->document->name . '.' . $extensions;
1003  
1004          return "$root/$documentCode/$version/$filename";
1005      }
1006  }