From dafa89883f56ccc8c36ff5212a496520463e56a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:22:20 +0000 Subject: [PATCH 1/3] Initial plan From 8be5b844c37222376d4ee2e7546e133e12d98c3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:31:48 +0000 Subject: [PATCH 2/3] Add file processing notifications feature Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- .env.demo | 1 + app/config.py | 4 + app/tasks/finalize_document_storage.py | 36 ++++- app/utils/notification.py | 35 +++++ docs/NotificationsSetup.md | 37 ++++++ tests/test_notifications.py | 175 +++++++++++++++++++++++++ 6 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 tests/test_notifications.py diff --git a/.env.demo b/.env.demo index b367e06c..7aaa0e90 100644 --- a/.env.demo +++ b/.env.demo @@ -146,6 +146,7 @@ NOTIFY_ON_TASK_FAILURE=True NOTIFY_ON_CREDENTIAL_FAILURE=True NOTIFY_ON_STARTUP=True NOTIFY_ON_SHUTDOWN=False +NOTIFY_ON_FILE_PROCESSED=True # Uptime Kuma UPTIME_KUMA_URL=https://status.example.com/api/push/abcdef123456?status=up diff --git a/app/config.py b/app/config.py index 6d8af64a..b723b74e 100644 --- a/app/config.py +++ b/app/config.py @@ -156,6 +156,10 @@ class Settings(BaseSettings): default=False, description="Send notifications when application shuts down" ) + notify_on_file_processed: bool = Field( + default=True, + description="Send notifications when files are successfully processed" + ) @validator('notification_urls', pre=True) def parse_notification_urls(cls, v): diff --git a/app/tasks/finalize_document_storage.py b/app/tasks/finalize_document_storage.py index 6d5216d0..b0c76db4 100644 --- a/app/tasks/finalize_document_storage.py +++ b/app/tasks/finalize_document_storage.py @@ -1,12 +1,16 @@ #!/usr/bin/env python3 +import os from app.config import settings from app.tasks.retry_config import BaseTaskWithRetry # Import the shared Celery instance from app.celery_app import celery # 1) Import the aggregator task -from app.tasks.send_to_all import send_to_all_destinations +from app.tasks.send_to_all import send_to_all_destinations, get_configured_services_from_validator + +# Import notification utility +from app.utils.notification import notify_file_processed @celery.task(base=BaseTaskWithRetry) @@ -14,12 +18,42 @@ def finalize_document_storage(original_file: str, processed_file: str, metadata: """ Final storage step after embedding metadata. We will now call 'send_to_all_destinations' to push the final PDF to Dropbox/Nextcloud/Paperless. + After uploading, send a notification about the processed file. """ print(f"[INFO] Finalizing document storage for {processed_file}") + # Determine which destinations are configured + configured_destinations = [] + try: + configured_services = get_configured_services_from_validator() + # Get list of service names that are configured + for service_name, is_configured in configured_services.items(): + if is_configured: + # Format service names for display + display_name = service_name.replace('_', ' ').title() + configured_destinations.append(display_name) + except Exception as e: + print(f"[WARNING] Could not determine configured destinations: {e}") + configured_destinations = ["configured destinations"] + # 2) Enqueue uploads to all destinations (Dropbox, Nextcloud, Paperless) send_to_all_destinations.delay(processed_file) + # 3) Send notification about successful file processing + try: + # Get file information + file_size = os.path.getsize(processed_file) if os.path.exists(processed_file) else 0 + filename = os.path.basename(processed_file) + + notify_file_processed( + filename=filename, + file_size=file_size, + metadata=metadata, + destinations=configured_destinations + ) + except Exception as e: + print(f"[WARNING] Failed to send file processed notification: {e}") + return { "status": "Completed", "file": processed_file diff --git a/app/utils/notification.py b/app/utils/notification.py index 1d07bd0b..e0347d8a 100644 --- a/app/utils/notification.py +++ b/app/utils/notification.py @@ -183,3 +183,38 @@ def notify_shutdown() -> bool: notification_type="info", tags=["system", "shutdown"] ) + +def notify_file_processed(filename: str, file_size: int, metadata: dict, destinations: list) -> bool: + """Send a notification that a file has been successfully processed""" + if not settings.notify_on_file_processed: + return False + + # Format file size for display + size_mb = file_size / (1024 * 1024) + size_str = f"{size_mb:.2f} MB" if size_mb >= 1 else f"{file_size / 1024:.2f} KB" + + # Extract key metadata fields + doc_type = metadata.get('document_type', 'Unknown') + tags = metadata.get('tags', []) + tags_str = ', '.join(tags) if tags else 'None' + + # Format destinations + destinations_str = ', '.join(destinations) if destinations else 'None configured' + + title = f"File Processed: {filename}" + message = f""" +File: {filename} +Size: {size_str} +Document Type: {doc_type} +Tags: {tags_str} +Destinations: {destinations_str} + +The file has been successfully processed and uploaded to all configured destinations. +""" + + return send_notification( + title=title, + message=message.strip(), + notification_type="success", + tags=["document", "processed", "success"] + ) diff --git a/docs/NotificationsSetup.md b/docs/NotificationsSetup.md index cf65c6a4..cc0f3c51 100644 --- a/docs/NotificationsSetup.md +++ b/docs/NotificationsSetup.md @@ -11,6 +11,7 @@ This guide explains how to set up the notification system for DocuElevate, which | `NOTIFY_ON_CREDENTIAL_FAILURE` | Send notifications on credential failures (`True`/`False`) | | `NOTIFY_ON_STARTUP` | Send notification when system starts (`True`/`False`) | | `NOTIFY_ON_SHUTDOWN` | Send notification when system shuts down (`True`/`False`)| +| `NOTIFY_ON_FILE_PROCESSED` | Send notifications when files are processed (`True`/`False`)| For a complete list of configuration options, see the [Configuration Guide](ConfigurationGuide.md). @@ -61,6 +62,42 @@ NOTIFY_ON_TASK_FAILURE=True # Notify when background tasks fail NOTIFY_ON_CREDENTIAL_FAILURE=True # Notify when service credentials fail (e.g. API token expired) NOTIFY_ON_STARTUP=True # Notify when the system starts NOTIFY_ON_SHUTDOWN=False # Notify when the system shuts down +NOTIFY_ON_FILE_PROCESSED=True # Notify when files are successfully processed +``` + +## File Processing Notifications + +DocuElevate can notify you whenever a document is successfully processed and uploaded to your configured destinations. When enabled, you'll receive a notification containing: + +- **File name**: The name of the processed document +- **File size**: Size of the processed file +- **Document type**: The type of document detected by AI (invoice, receipt, contract, etc.) +- **Tags**: Any tags extracted from the document +- **Destinations**: List of destinations where the file was uploaded + +This feature is particularly useful for: +- Tracking document processing in real-time +- Confirming important documents have been processed +- Monitoring the document processing workflow +- Receiving immediate feedback when uploading documents via email or API + +To enable file processing notifications: + +```dotenv +NOTIFY_ON_FILE_PROCESSED=True +``` + +Example notification: +``` +Subject: File Processed: Invoice_2024-01-15.pdf + +File: Invoice_2024-01-15.pdf +Size: 1.23 MB +Document Type: Invoice +Tags: financial, vendor-acme, urgent +Destinations: Dropbox, Google Drive, Paperless-ngx + +The file has been successfully processed and uploaded to all configured destinations. ``` ## Automated Credential Checking diff --git a/tests/test_notifications.py b/tests/test_notifications.py new file mode 100644 index 00000000..6bf4eadf --- /dev/null +++ b/tests/test_notifications.py @@ -0,0 +1,175 @@ +import os +import unittest +from unittest.mock import patch, MagicMock +from app.utils.notification import notify_file_processed, send_notification +from app.config import settings + + +class TestFileProcessedNotification(unittest.TestCase): + """Test file processing notification functionality""" + + @patch('app.utils.notification.send_notification') + def test_notify_file_processed_when_enabled(self, mock_send): + """Test that notification is sent when NOTIFY_ON_FILE_PROCESSED is True""" + # Arrange + mock_send.return_value = True + original_value = settings.notify_on_file_processed + settings.notify_on_file_processed = True + + filename = "test_document.pdf" + file_size = 1024 * 1024 # 1 MB + metadata = { + 'document_type': 'Invoice', + 'tags': ['financial', 'urgent'] + } + destinations = ['Dropbox', 'Google Drive'] + + try: + # Act + result = notify_file_processed(filename, file_size, metadata, destinations) + + # Assert + self.assertTrue(result) + mock_send.assert_called_once() + + # Check the call arguments + call_args = mock_send.call_args + self.assertIn("test_document.pdf", call_args[1]['title']) + self.assertIn("Invoice", call_args[1]['message']) + self.assertIn("financial, urgent", call_args[1]['message']) + self.assertIn("Dropbox, Google Drive", call_args[1]['message']) + self.assertEqual(call_args[1]['notification_type'], "success") + finally: + settings.notify_on_file_processed = original_value + + @patch('app.utils.notification.send_notification') + def test_notify_file_processed_when_disabled(self, mock_send): + """Test that notification is not sent when NOTIFY_ON_FILE_PROCESSED is False""" + # Arrange + original_value = settings.notify_on_file_processed + settings.notify_on_file_processed = False + + filename = "test_document.pdf" + file_size = 1024 * 1024 + metadata = {'document_type': 'Invoice', 'tags': []} + destinations = ['Dropbox'] + + try: + # Act + result = notify_file_processed(filename, file_size, metadata, destinations) + + # Assert + self.assertFalse(result) + mock_send.assert_not_called() + finally: + settings.notify_on_file_processed = original_value + + @patch('app.utils.notification.send_notification') + def test_notify_file_processed_with_no_destinations(self, mock_send): + """Test notification when no destinations are configured""" + # Arrange + mock_send.return_value = True + original_value = settings.notify_on_file_processed + settings.notify_on_file_processed = True + + filename = "test_document.pdf" + file_size = 512 * 1024 # 512 KB + metadata = { + 'document_type': 'Receipt', + 'tags': [] + } + destinations = [] + + try: + # Act + result = notify_file_processed(filename, file_size, metadata, destinations) + + # Assert + self.assertTrue(result) + mock_send.assert_called_once() + + # Check that message indicates no destinations + call_args = mock_send.call_args + self.assertIn("None configured", call_args[1]['message']) + finally: + settings.notify_on_file_processed = original_value + + @patch('app.utils.notification.send_notification') + def test_notify_file_processed_formats_file_size_mb(self, mock_send): + """Test that file size is formatted correctly for MB""" + # Arrange + mock_send.return_value = True + original_value = settings.notify_on_file_processed + settings.notify_on_file_processed = True + + filename = "large_document.pdf" + file_size = 5 * 1024 * 1024 # 5 MB + metadata = {'document_type': 'Contract', 'tags': []} + destinations = ['Dropbox'] + + try: + # Act + result = notify_file_processed(filename, file_size, metadata, destinations) + + # Assert + self.assertTrue(result) + call_args = mock_send.call_args + self.assertIn("5.00 MB", call_args[1]['message']) + finally: + settings.notify_on_file_processed = original_value + + @patch('app.utils.notification.send_notification') + def test_notify_file_processed_formats_file_size_kb(self, mock_send): + """Test that file size is formatted correctly for KB""" + # Arrange + mock_send.return_value = True + original_value = settings.notify_on_file_processed + settings.notify_on_file_processed = True + + filename = "small_document.pdf" + file_size = 512 * 1024 # 512 KB + metadata = {'document_type': 'Note', 'tags': []} + destinations = ['Dropbox'] + + try: + # Act + result = notify_file_processed(filename, file_size, metadata, destinations) + + # Assert + self.assertTrue(result) + call_args = mock_send.call_args + self.assertIn("512.00 KB", call_args[1]['message']) + finally: + settings.notify_on_file_processed = original_value + + @patch('app.utils.notification.send_notification') + def test_notify_file_processed_with_missing_metadata_fields(self, mock_send): + """Test that notification handles missing metadata fields gracefully""" + # Arrange + mock_send.return_value = True + original_value = settings.notify_on_file_processed + settings.notify_on_file_processed = True + + filename = "document.pdf" + file_size = 1024 * 1024 + metadata = {} # Empty metadata + destinations = ['Dropbox'] + + try: + # Act + result = notify_file_processed(filename, file_size, metadata, destinations) + + # Assert + self.assertTrue(result) + mock_send.assert_called_once() + + # Check that defaults are used + call_args = mock_send.call_args + self.assertIn("Unknown", call_args[1]['message']) # Default document type + self.assertIn("None", call_args[1]['message']) # No tags + finally: + settings.notify_on_file_processed = original_value + + +if __name__ == '__main__': + unittest.main() From 9c4cf3d70ec8eba9ac631e3a3c39a4983f635ab4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:33:16 +0000 Subject: [PATCH 3/3] Fix notification message to accurately reflect async upload timing Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/tasks/finalize_document_storage.py | 3 +++ app/utils/notification.py | 2 +- docs/NotificationsSetup.md | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/tasks/finalize_document_storage.py b/app/tasks/finalize_document_storage.py index b0c76db4..e81a4be3 100644 --- a/app/tasks/finalize_document_storage.py +++ b/app/tasks/finalize_document_storage.py @@ -37,9 +37,12 @@ def finalize_document_storage(original_file: str, processed_file: str, metadata: configured_destinations = ["configured destinations"] # 2) Enqueue uploads to all destinations (Dropbox, Nextcloud, Paperless) + # Note: send_to_all_destinations is asynchronous and queues upload tasks send_to_all_destinations.delay(processed_file) # 3) Send notification about successful file processing + # Note: This notification is sent after processing is complete but while uploads + # are being queued. The message reflects that uploads are being initiated. try: # Get file information file_size = os.path.getsize(processed_file) if os.path.exists(processed_file) else 0 diff --git a/app/utils/notification.py b/app/utils/notification.py index e0347d8a..d96a77bc 100644 --- a/app/utils/notification.py +++ b/app/utils/notification.py @@ -209,7 +209,7 @@ Document Type: {doc_type} Tags: {tags_str} Destinations: {destinations_str} -The file has been successfully processed and uploaded to all configured destinations. +The file has been successfully processed and is being uploaded to all configured destinations. """ return send_notification( diff --git a/docs/NotificationsSetup.md b/docs/NotificationsSetup.md index cc0f3c51..472ffccf 100644 --- a/docs/NotificationsSetup.md +++ b/docs/NotificationsSetup.md @@ -97,7 +97,7 @@ Document Type: Invoice Tags: financial, vendor-acme, urgent Destinations: Dropbox, Google Drive, Paperless-ngx -The file has been successfully processed and uploaded to all configured destinations. +The file has been successfully processed and is being uploaded to all configured destinations. ``` ## Automated Credential Checking