*/ protected $fillable = [ 'document_id', 'file_path', 'hash', 'version', 'user_id', 'changes', ]; /** * The attributes that should be cast. * * @var array */ protected $casts = [ 'changes' => 'array', 'version' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; /** * The attributes that should be appended to the model's array form. * * @var array */ protected $appends = [ 'file_url', 'file_size_formatted', 'created_at_formatted', ]; /** * Boot function for model events */ protected static function boot() { parent::boot(); static::creating(function ($model) { // Asegurar que la versión sea incremental para el mismo documento if (empty($model->version)) { $lastVersion = self::where('document_id', $model->document_id) ->max('version'); $model->version = $lastVersion ? $lastVersion + 1 : 1; } }); static::deleting(function ($model) { // No eliminar el archivo físico si hay otras versiones que lo usan $sameFileCount = self::where('file_path', $model->file_path) ->where('id', '!=', $model->id) ->count(); if ($sameFileCount === 0 && Storage::exists($model->file_path)) { Storage::delete($model->file_path); } }); } /** * Get the document that owns the version. */ public function document(): BelongsTo { return $this->belongsTo(Document::class); } /** * Get the user who created the version. */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** * Get the file URL for the version. */ public function getFileUrlAttribute(): string { return Storage::url($this->file_path); } /** * Get the file size in a human-readable format. */ public function getFileSizeFormattedAttribute(): string { if (!Storage::exists($this->file_path)) { return '0 B'; } $bytes = Storage::size($this->file_path); $units = ['B', 'KB', 'MB', 'GB', 'TB']; $index = 0; while ($bytes >= 1024 && $index < count($units) - 1) { $bytes /= 1024; $index++; } return round($bytes, 2) . ' ' . $units[$index]; } /** * Get the actual file size in bytes. */ public function getFileSizeAttribute(): int { return Storage::exists($this->file_path) ? Storage::size($this->file_path) : 0; } /** * Get formatted created_at date. */ public function getCreatedAtFormattedAttribute(): string { return $this->created_at->format('d/m/Y H:i'); } /** * Get the version label (v1, v2, etc.) */ public function getVersionLabelAttribute(): string { return 'v' . $this->version; } /** * Check if this is the current version of the document. */ public function getIsCurrentAttribute(): bool { return $this->document->current_version_id === $this->id; } /** * Get changes summary for display. */ public function getChangesSummaryAttribute(): string { if (empty($this->changes)) { return 'Sin cambios registrados'; } $changes = $this->changes; if (is_array($changes)) { $summary = []; if (isset($changes['annotations_count']) && $changes['annotations_count'] > 0) { $summary[] = $changes['annotations_count'] . ' anotación(es)'; } if (isset($changes['signatures_count']) && $changes['signatures_count'] > 0) { $summary[] = $changes['signatures_count'] . ' firma(s)'; } if (isset($changes['stamps_count']) && $changes['stamps_count'] > 0) { $summary[] = $changes['stamps_count'] . ' sello(s)'; } if (isset($changes['edited_by'])) { $summary[] = 'por ' . $changes['edited_by']; } return implode(', ', $summary); } return (string) $changes; } /** * Scope a query to only include versions of a specific document. */ public function scopeOfDocument($query, $documentId) { return $query->where('document_id', $documentId); } /** * Scope a query to order versions by latest first. */ public function scopeLatestFirst($query) { return $query->orderBy('version', 'desc'); } /** * Scope a query to order versions by oldest first. */ public function scopeOldestFirst($query) { return $query->orderBy('version', 'asc'); } /** * Get the previous version. */ public function getPreviousVersion(): ?self { return self::where('document_id', $this->document_id) ->where('version', '<', $this->version) ->orderBy('version', 'desc') ->first(); } /** * Get the next version. */ public function getNextVersion(): ?self { return self::where('document_id', $this->document_id) ->where('version', '>', $this->version) ->orderBy('version', 'asc') ->first(); } /** * Check if file exists in storage. */ public function fileExists(): bool { return Storage::exists($this->file_path); } /** * Get file content. */ public function getFileContent(): ?string { return $this->fileExists() ? Storage::get($this->file_path) : null; } /** * Verify file integrity using hash. */ public function verifyIntegrity(): bool { if (!$this->fileExists()) { return false; } $currentHash = hash_file('sha256', Storage::path($this->file_path)); return $currentHash === $this->hash; } /** * Create a new version from file content. */ public static function createFromContent(Document $document, string $content, array $changes = [], ?User $user = null): self { $user = $user ?: auth()->user(); // Calcular hash $hash = hash('sha256', $content); // Determinar siguiente versión $lastVersion = self::where('document_id', $document->id)->max('version'); $version = $lastVersion ? $lastVersion + 1 : 1; // Guardar archivo $filePath = "documents/{$document->id}/versions/v{$version}.pdf"; Storage::put($filePath, $content); // Crear registro return self::create([ 'document_id' => $document->id, 'file_path' => $filePath, 'hash' => $hash, 'version' => $version, 'user_id' => $user->id, 'changes' => $changes, ]); } /** * Restore this version as the current version. */ public function restoreAsCurrent(): bool { if (!$this->fileExists()) { return false; } // Crear una nueva versión idéntica a esta $content = $this->getFileContent(); $newVersion = self::createFromContent( $this->document, $content, ['restored_from' => 'v' . $this->version] ); // Actualizar documento para apuntar a la nueva versión $this->document->update([ 'current_version_id' => $newVersion->id ]); return true; } }