Webshop - Order Documents, Virus Scan, And Infected Notifications¶
Overview¶
This document specifies the missing order-document portal and malware handling behavior currently present in legacy code but not yet documented in migration specs.
Checklist coverage:
- WF-029 Order document portal (tokenized upload/download/zip)
- OR-005 Order documents module
- OR-006 Infected document notifications module
- OR-007 VirusTotal scanning integration
- OR-008 Retry Odoo document upload command
Primary legacy evidence:
- _core/config/router.php
- src/Atraxion/Module/OrderDocuments/*
- src/Atraxion/Module/OrderDocuments/Command/RetryOdooUploadsCommand.php
- src/Atraxion/Module/OrderDocuments/Repository/OrderDocumentRepository.php
- src/Atraxion/Module/OrderDocuments/Service/OdooDocumentUploadService.php
- src/Atraxion/Module/InfectedDocumentNotification/*
- src/VirusTotal/*
- _core/controller/classes/atx/orders/OrdersController.php
- _core/view/classes/atx/orders/OrdersView.php
- _core/model/classes/atx/orders/OrderService.php
- _core/config/doctrine/migrations/2025/09/*, _core/config/doctrine/migrations/2025/10/*
Legacy Capabilities (As-Is)¶
Entry Points¶
- Tokenized public portal routes (
/order-documents/{orderHash}/{token}): GET /order-documents/{orderHash}/{token}view portalPOST /order-documents/{orderHash}/{token}/uploadupload documentGET /order-documents/{orderHash}/{token}/download/{documentId}download one-
GET /order-documents/{orderHash}/{token}/download-alldownload ZIP -
Logged-in customer order dialog actions (
OrdersControllerAjax actions): orders_uploadDocumentorders_getDocumentsorders_deleteDocument
Access Model¶
Portal access requires:
- Order found by OrderHash (fallback to numeric OrderId)
- documentPortalToken exact match
- documentPortalExpiry not expired
Logged-in order-dialog access requires:
- Authenticated user
- Order ownership validation
- Customer tag permission CustomerTag::enableDocumentPortal
Upload Rules¶
OrderDocumentService validation:
- Upload must be successful (UPLOAD_ERR_OK)
- Max size 5MB
- Non-empty file
- MIME type must be application/pdf
- Extension must be .pdf
Storage:
- Physical path: APP_STATIC_PATH/order_documents/{YYYY}/{MM}/{orderId}/{uniqueFilename}
- Original filename retained for display/download name
- uploadedBy source is constrained to customer|warehouse
Data Model¶
order_documents includes:
- File metadata (filename, original_filename, file_size, mime_type, uploaded_by)
- Virus scan state:
- virus_scan_status (pending|scanning|clean|infected|error)
- virus_total_analysis_id
- virus_scan_completed_at
- virus_scan_result
- is_quarantined
- Odoo upload state:
- odoo_upload_status (pending|uploaded|failed)
- odoo_uploaded_at
- odoo_upload_attempts
- odoo_upload_last_error
- odoo_sale_order_attachment_id
- odoo_stock_picking_attachment_id
infected_document_notifications includes:
- order_document_id
- recipient (recipient_email, recipient_name)
- status (pending|sent|failed)
- error_message
- timestamps (created_at, sent_at, updated_at)
Virus Scan Workflow¶
Upload-time behavior:
1. Document row is persisted.
2. If VirusScanner exists, file is submitted to VirusTotal.
3. virus_total_analysis_id is saved and status set to scanning.
4. Upload itself is not failed when scan submission fails.
Processing-time behavior (order-documents:check-virus-scans):
1. Poll documents with ongoing scan status.
2. If clean:
- Mark clean
- Store scan result payload
- Attempt Odoo upload
3. If infected:
- Mark infected
- Delete physical file
- Attempt to create infected-document notifications
Odoo Upload Behavior¶
Clean non-quarantined docs are eligible.
Upload targets:
1. sale.order attachment via xx_external_id = order_id
2. stock.picking attachment when delivery note / stock picking id exists
Result handling: - Sale-order upload success is considered overall success - Stock-picking upload can fail without failing sale-order success - Upload attempts and last error are persisted
Odoo Upload Retry Command (OR-008)¶
Command:
- order-documents:retry-odoo-uploads
Selection modes:
1. Default mode retries failed uploads only (findFailedOdooUploads).
2. --all mode processes both pending and failed clean/non-quarantined docs (findDocumentsReadyForOdooUpload).
3. --document-id={id} mode retries one specific document.
Execution behavior:
1. Skips documents when odooUploadAttempts >= --max-attempts (default 5).
2. Increments attempt counter before each upload attempt.
3. Fails immediately for missing file path and stores File not found as last error.
4. Calls OdooDocumentUploadService::uploadDocumentToOdoo.
5. Persists status per record (uploaded or failed) and logs metrics in command output.
Return semantics:
- Returns FAILURE if one or more uploads fail.
- Returns SUCCESS when all processed items succeed or when no candidate records are found.
Infected Notification Behavior¶
Notification creation: - Triggered from virus-check command when file is infected - Creates one notification row per recipient if not already existing - Default recipients are hardcoded in service
Notification sending:
- Command: infected-documents:send-notifications
- Sends pending, optional retry for failed
- Marks each notification as sent/failed
Current Gaps And Risks To Resolve In Migration¶
Observed in current legacy implementation:
- Notification service is optional in
CheckVirusScanResultsCommand, but command provider wiring does not pass it, so infected notifications can remain uncreated. - Infected handling deletes file but does not set
is_quarantined = true, while schema and docs indicate quarantine semantics. - Portal download endpoints do not enforce
isSafeToAccess(), so unsafe files are not explicitly blocked by status checks. - ZIP creation includes all files found for an order, without scan-status filtering.
--submit-pendingscan path submits files but does not persist analysis ID/status changes from that path.- Infected notification mail body uses
getFileName()while the document model exposesgetFilename()/getOriginalFilename(). - Retry command has no lock/idempotency guard, so concurrent runs can duplicate work and attempt counters.
- Retry command stores a flush per document update, which may be slow for large batches.
- Retry command does not expose structured per-status exit metrics to scheduler tooling beyond console text.
Target Migration Specification (Symfony)¶
Scope¶
In-scope for this phase: - Tokenized portal for upload/download - Authenticated customer document management - Virus scan orchestration and status persistence - Infected notification queue and sender - Odoo upload of clean files - Odoo upload retry/recovery workflow for failed and pending documents
Out-of-scope for this phase: - Full SOC workflow / SIEM integration - Multi-scanner provider switching logic
Domain Rules To Preserve¶
- Portal links are tokenized and time-bound.
- Customer access is constrained to own orders.
- Upload accepts PDFs only (max 5MB unless changed by explicit decision).
- Odoo upload only for scan-clean and non-quarantined documents.
- Duplicate infected notifications for same recipient+document are prevented.
Required Improvements¶
- Enforce safe-download policy: block or hide files not in
cleanstate. - Make quarantine behavior explicit and consistent (state + storage handling).
- Ensure infected notification creation is guaranteed when infection is detected.
- Persist scan submission transitions consistently in all command modes.
- Replace hardcoded recipients with configurable recipients per environment/tenant policy.
- Add idempotency and locking around scan-result processing to avoid double handling.
- Add locking/backoff semantics for Odoo retry command to avoid overlapping retries.
- Emit machine-readable run summary (success/failure/skipped) for scheduler observability.
Suggested Target Components¶
Application services:
- DocumentPortalAccessService
- OrderDocumentUploadService
- DocumentScanService
- InfectedDocumentNotificationService
- OdooDocumentUploadService
Background jobs:
- order-documents:scan-submit (optional split)
- order-documents:scan-poll
- order-documents:upload-odoo-retry
- order-documents:notify-infected
Entities:
- OrderDocument
- InfectedDocumentNotification
Security Requirements¶
- Keep token validation and expiry checks for portal links.
- Remove fallback from hash to sequential
OrderIdin public URLs. - Store file names safely and prevent path traversal.
- Restrict MIME acceptance with server-side sniffing, not client mime only.
- Add audit log for upload, download, delete, quarantine, and Odoo upload actions.
Operational Requirements¶
- Define scan polling frequency and max batch size per environment.
- Track metrics:
- uploads total
- scans pending/scanning/clean/infected/error
- Odoo upload success/failure
- infected notifications pending/failed
- Alert on:
- infected detections
- repeated Odoo upload failures
- scan backlogs over threshold
Acceptance Scenarios (Gherkin)¶
Feature: Order document portal and malware workflow
Scenario: Open valid tokenized portal
Given an order has a valid document portal token and non-expired link
When I open /order-documents/{orderHash}/{token}
Then I should see the document portal with existing order documents
Scenario: Deny expired tokenized portal
Given an order document portal token is expired
When I open /order-documents/{orderHash}/{token}
Then I should receive a 403 response and an expired-link page
Scenario: Upload valid PDF from portal
Given I am on a valid portal link
When I upload a 2MB PDF file
Then a document record should be created with virus scan status "scanning" or "pending"
Scenario: Reject invalid upload type
Given I am on a valid portal link
When I upload a .jpg file
Then I should receive a validation error that only PDF is allowed
Scenario: Customer order dialog upload is ownership-protected
Given I am logged in as customer A
When I upload a document for an order owned by customer B
Then the upload should be denied
Scenario: Clean scan uploads to Odoo
Given a document scan completes as clean
When scan processing runs
Then the document should be marked clean
And Odoo upload should be attempted
Scenario: Infected scan quarantines and notifies
Given a document scan completes as infected
When scan processing runs
Then the physical file should be quarantined or removed according to policy
And infected notification rows should be created
Scenario: Infected notifications are delivered
Given there are pending infected notifications
When infected-documents:send-notifications runs
Then notifications should be marked sent or failed with error details
Scenario: Failed Odoo uploads are retried with max-attempt guard
Given there are clean non-quarantined documents with odoo_upload_status "failed"
And a document already has 5 attempts
When order-documents:retry-odoo-uploads runs with max-attempts=5
Then that document should be skipped
And documents below the attempt limit should be retried
Scenario: Unsafe documents are not downloadable
Given a document status is pending, scanning, infected, or error
When I try to download it from portal
Then access should be denied
Data Migration Notes¶
- Preserve
orders.documentPortalTokenandorders.documentPortalExpiryvalues. - Preserve all
order_documentsrows and files. - Preserve
infected_document_notificationshistory. - Backfill any legacy missing timestamps or statuses with explicit migration rules.
- If quarantine policy changes, migrate existing infected records accordingly.
Open Decisions¶
- Should portal links remain valid for 30 days or become configurable?
- Should unsafe files be hard-blocked for all users, including admins?
- Should infected files be hard-deleted or moved to a dedicated quarantine store?
- Should recipient policy for infected alerts be global, per tenant, or per environment?
- Should scan and Odoo upload run in synchronous cron commands or queue workers?