disk = Storage::disk('s3'); } /** * Upload record attachment file * * @param UploadedFile $attachmentFile * @param int $recordId * @param string $recordType ('IN' or 'OUT') * @return string * @throws Exception */ public function uploadAttachment(UploadedFile $attachmentFile, int $recordId, string $recordType = 'OUT'): string { try { $currentClient = session('currentClient', 'iao'); // Validate file $this->validateAttachmentFile($attachmentFile); // Create filename $originalName = pathinfo($attachmentFile->getClientOriginalName(), PATHINFO_FILENAME); $extension = strtolower($attachmentFile->getClientOriginalExtension()); $timestamp = time(); $filename = 'attachment_' . $recordId . '_' . $timestamp . '_' . substr(md5($originalName), 0, 8) . '.' . $extension; // Upload to S3 $s3Path = $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/attachments/' . $filename; $uploaded = $this->disk->putFileAs( $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/attachments', $attachmentFile, $filename, 'private' ); if (!$uploaded) { throw new Exception('Failed to upload attachment to S3: ' . $originalName); } Log::info("Record attachment uploaded", [ 'record_id' => $recordId, 'record_type' => $recordType, 'path' => $s3Path, 'original_name' => $attachmentFile->getClientOriginalName(), 'size' => $attachmentFile->getSize() ]); return $s3Path; } catch (Exception $e) { Log::error("Error uploading record attachment", [ 'record_id' => $recordId, 'record_type' => $recordType, 'error' => $e->getMessage() ]); throw $e; } } /** * Upload XML receipt file (for electronic invoice imports) * * @param UploadedFile $xmlFile * @param int $recordId * @param string $recordType * @return string * @throws Exception */ public function uploadXmlReceipt(UploadedFile $xmlFile, int $recordId, string $recordType = 'OUT'): string { try { $currentClient = session('currentClient', 'iao'); // Validate XML file $this->validateXmlFile($xmlFile); // Create filename $originalName = pathinfo($xmlFile->getClientOriginalName(), PATHINFO_FILENAME); $extension = strtolower($xmlFile->getClientOriginalExtension()); $timestamp = time(); $filename = 'xml_receipt_' . $recordId . '_' . $timestamp . '_' . substr(md5($originalName), 0, 8) . '.' . $extension; // Upload to S3 $s3Path = $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/xml/' . $filename; $uploaded = $this->disk->putFileAs( $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/xml', $xmlFile, $filename, 'private' ); if (!$uploaded) { throw new Exception('Failed to upload XML receipt to S3: ' . $originalName); } Log::info("XML receipt uploaded", [ 'record_id' => $recordId, 'record_type' => $recordType, 'path' => $s3Path, 'original_name' => $xmlFile->getClientOriginalName(), 'size' => $xmlFile->getSize() ]); return $s3Path; } catch (Exception $e) { Log::error("Error uploading XML receipt", [ 'record_id' => $recordId, 'record_type' => $recordType, 'error' => $e->getMessage() ]); throw $e; } } /** * Get attachment file URL for display/download * * @param string $filePath * @param string $expiresIn * @return string|null */ public function getAttachmentUrl(string $filePath, string $expiresIn = '+1 hour'): ?string { if (!$filePath) { return null; } // Handle legacy local paths - return asset URL if (!$this->isS3Path($filePath)) { return asset('storage/' . $filePath); } try { if (!$this->disk->exists($filePath)) { Log::warning("Attachment file not found", ['path' => $filePath]); return null; } return $this->disk->temporaryUrl($filePath, now()->add($expiresIn)); } catch (Exception $e) { Log::error("Error generating attachment URL", [ 'path' => $filePath, 'error' => $e->getMessage() ]); return null; } } /** * Delete attachment file from S3 * * @param string $filePath * @return bool */ public function deleteAttachment(string $filePath): bool { // Don't try to delete local files if (!$this->isS3Path($filePath)) { return false; } try { if ($this->disk->exists($filePath)) { $this->disk->delete($filePath); Log::info("Attachment file deleted", ['path' => $filePath]); return true; } return false; } catch (Exception $e) { Log::error("Error deleting attachment file", [ 'path' => $filePath, 'error' => $e->getMessage() ]); return false; } } /** * Check if path is S3 path */ private function isS3Path(string $path): bool { return strpos($path, '/records/') !== false || strpos($path, session('currentClient', 'iao')) === 0; } /** * Validate the uploaded attachment file * * @param UploadedFile $attachmentFile * @throws Exception */ private function validateAttachmentFile(UploadedFile $attachmentFile): void { // Check if file is valid if (!$attachmentFile->isValid()) { throw new Exception('Invalid file upload'); } // Check file size if ($attachmentFile->getSize() > (self::MAX_FILE_SIZE * 1024)) { throw new Exception('File size exceeds maximum allowed size of ' . self::MAX_FILE_SIZE . 'KB'); } // Check file extension $extension = strtolower($attachmentFile->getClientOriginalExtension()); if (!in_array($extension, self::ALLOWED_EXTENSIONS)) { throw new Exception('File type not allowed. Allowed types: ' . implode(', ', self::ALLOWED_EXTENSIONS)); } // Check mime type for additional security $mimeType = $attachmentFile->getMimeType(); $allowedMimeTypes = [ 'image/jpeg', 'image/png', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/xml', 'text/xml', 'text/plain', 'text/csv', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ]; if (!in_array($mimeType, $allowedMimeTypes)) { throw new Exception('Invalid file type'); } } /** * Validate XML file specifically * * @param UploadedFile $xmlFile * @throws Exception */ private function validateXmlFile(UploadedFile $xmlFile): void { // Check if file is valid if (!$xmlFile->isValid()) { throw new Exception('Invalid XML file upload'); } // Check file size if ($xmlFile->getSize() > (self::MAX_FILE_SIZE * 1024)) { throw new Exception('XML file size exceeds maximum allowed size of ' . self::MAX_FILE_SIZE . 'KB'); } // Check file extension $extension = strtolower($xmlFile->getClientOriginalExtension()); if ($extension !== 'xml') { throw new Exception('Only XML files are allowed for receipt uploads'); } // Check mime type $mimeType = $xmlFile->getMimeType(); $allowedMimeTypes = ['application/xml', 'text/xml']; if (!in_array($mimeType, $allowedMimeTypes)) { throw new Exception('Invalid XML file type'); } } /** * Create record folder structure in S3 * * @param int $recordId * @param string $recordType */ public function createRecordFolders(int $recordId, string $recordType = 'OUT'): void { try { $currentClient = session('currentClient', 'iao'); $folders = [ $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/attachments/.gitkeep', $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/xml/.gitkeep' ]; foreach ($folders as $folder) { if (!$this->disk->exists($folder)) { $this->disk->put($folder, ''); } } Log::info("Created record folder structure", [ 'record_id' => $recordId, 'record_type' => $recordType ]); } catch (Exception $e) { Log::error("Error creating record folders", [ 'record_id' => $recordId, 'record_type' => $recordType, 'error' => $e->getMessage() ]); } } /** * Get attachment file info * * @param string $filePath * @return array|null */ public function getAttachmentInfo(string $filePath): ?array { if (!$filePath) { return null; } // Handle legacy local paths if (!$this->isS3Path($filePath)) { $localPath = storage_path('app/public/' . $filePath); if (file_exists($localPath)) { return [ 'path' => $filePath, 'name' => basename($filePath), 'size' => filesize($localPath), 'last_modified' => filemtime($localPath), 'url' => $this->getAttachmentUrl($filePath), 'exists' => true, 'storage_type' => 'local' ]; } return null; } if (!$this->disk->exists($filePath)) { return null; } try { return [ 'path' => $filePath, 'name' => basename($filePath), 'size' => $this->disk->size($filePath), 'last_modified' => $this->disk->lastModified($filePath), 'url' => $this->getAttachmentUrl($filePath), 'exists' => true, 'storage_type' => 's3' ]; } catch (Exception $e) { Log::error("Error getting attachment info", [ 'path' => $filePath, 'error' => $e->getMessage() ]); return null; } } /** * List all attachments for a record * * @param int $recordId * @param string $recordType * @return array */ public function listRecordAttachments(int $recordId, string $recordType = 'OUT'): array { try { $currentClient = session('currentClient', 'iao'); $attachmentPath = $currentClient . '/records/' . strtolower($recordType) . '/' . $recordId . '/attachments'; $files = $this->disk->files($attachmentPath); return array_filter($files, function($file) { $filename = basename($file); return strpos($filename, 'attachment_') === 0; }); } catch (Exception $e) { Log::error("Error listing record attachments", [ 'record_id' => $recordId, 'record_type' => $recordType, 'error' => $e->getMessage() ]); return []; } } /** * Store XML receipt files for batch import * * @param array $xmlFiles * @param string $batchId * @return array */ public function storeXmlBatch(array $xmlFiles, string $batchId): array { $storedFiles = []; $currentClient = session('currentClient', 'iao'); foreach ($xmlFiles as $index => $xmlFile) { try { $this->validateXmlFile($xmlFile); $originalName = pathinfo($xmlFile->getClientOriginalName(), PATHINFO_FILENAME); $extension = strtolower($xmlFile->getClientOriginalExtension()); $timestamp = time(); $filename = 'batch_' . $batchId . '_' . $index . '_' . $timestamp . '_' . substr(md5($originalName), 0, 8) . '.' . $extension; $s3Path = $currentClient . '/imports/xml_batch/' . $batchId . '/' . $filename; $uploaded = $this->disk->putFileAs( $currentClient . '/imports/xml_batch/' . $batchId, $xmlFile, $filename, 'private' ); if ($uploaded) { $storedFiles[] = [ 'original_name' => $xmlFile->getClientOriginalName(), 's3_path' => $s3Path, 'local_path' => $xmlFile->getRealPath(), 'index' => $index ]; Log::info("XML file stored for batch processing", [ 'batch_id' => $batchId, 'original_name' => $xmlFile->getClientOriginalName(), 's3_path' => $s3Path ]); } } catch (Exception $e) { Log::error("Error storing XML file for batch", [ 'batch_id' => $batchId, 'file_name' => $xmlFile->getClientOriginalName(), 'error' => $e->getMessage() ]); } } return $storedFiles; } }