'boolean', 'file_size' => 'integer', 'page_count' => 'integer', 'processing_started_at' => 'datetime', 'processing_completed_at' => 'datetime', ]; // Status constants const STATUS_PENDING = 'pending'; const STATUS_PROCESSING = 'processing'; const STATUS_EXTRACTING = 'extracting'; const STATUS_CHUNKING = 'chunking'; const STATUS_EMBEDDING = 'embedding'; const STATUS_INDEXED = 'indexed'; const STATUS_FAILED = 'failed'; const STATUS_EXTRACTION_FAILED = 'extraction_failed'; // === Relationships === public function document(): BelongsTo { return $this->belongsTo(Document::class); } public function chunks(): HasMany { return $this->hasMany(DocumentChunk::class)->orderBy('chunk_index'); } public function uploader(): BelongsTo { return $this->belongsTo(User::class, 'uploaded_by'); } // === Scopes === public function scopeCurrent(Builder $query): Builder { return $query->where('is_current', true); } public function scopeIndexed(Builder $query): Builder { return $query->where('processing_status', self::STATUS_INDEXED); } // === Helpers === public function getStorageUrl(): string { return Storage::url($this->stored_path); } public function getFileSizeFormattedAttribute(): string { $bytes = $this->file_size; if ($bytes < 1024) return $bytes . ' B'; if ($bytes < 1048576) return round($bytes / 1024, 1) . ' KB'; return round($bytes / 1048576, 1) . ' MB'; } public function isProcessed(): bool { return $this->processing_status === self::STATUS_INDEXED; } public function hasFailed(): bool { return in_array($this->processing_status, [ self::STATUS_FAILED, self::STATUS_EXTRACTION_FAILED, ]); } public function updateStatus(string $status, ?string $error = null): void { $data = ['processing_status' => $status]; if ($status === self::STATUS_PROCESSING) { $data['processing_started_at'] = now(); } if ($status === self::STATUS_INDEXED) { $data['processing_completed_at'] = now(); } if ($error !== null) { $data['processing_error'] = $error; } $this->update($data); } }