Skip to content

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

  1. Tokenized public portal routes (/order-documents/{orderHash}/{token}):
  2. GET /order-documents/{orderHash}/{token} view portal
  3. POST /order-documents/{orderHash}/{token}/upload upload document
  4. GET /order-documents/{orderHash}/{token}/download/{documentId} download one
  5. GET /order-documents/{orderHash}/{token}/download-all download ZIP

  6. Logged-in customer order dialog actions (OrdersController Ajax actions):

  7. orders_uploadDocument
  8. orders_getDocuments
  9. orders_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:

  1. Notification service is optional in CheckVirusScanResultsCommand, but command provider wiring does not pass it, so infected notifications can remain uncreated.
  2. Infected handling deletes file but does not set is_quarantined = true, while schema and docs indicate quarantine semantics.
  3. Portal download endpoints do not enforce isSafeToAccess(), so unsafe files are not explicitly blocked by status checks.
  4. ZIP creation includes all files found for an order, without scan-status filtering.
  5. --submit-pending scan path submits files but does not persist analysis ID/status changes from that path.
  6. Infected notification mail body uses getFileName() while the document model exposes getFilename() / getOriginalFilename().
  7. Retry command has no lock/idempotency guard, so concurrent runs can duplicate work and attempt counters.
  8. Retry command stores a flush per document update, which may be slow for large batches.
  9. 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

  1. Portal links are tokenized and time-bound.
  2. Customer access is constrained to own orders.
  3. Upload accepts PDFs only (max 5MB unless changed by explicit decision).
  4. Odoo upload only for scan-clean and non-quarantined documents.
  5. Duplicate infected notifications for same recipient+document are prevented.

Required Improvements

  1. Enforce safe-download policy: block or hide files not in clean state.
  2. Make quarantine behavior explicit and consistent (state + storage handling).
  3. Ensure infected notification creation is guaranteed when infection is detected.
  4. Persist scan submission transitions consistently in all command modes.
  5. Replace hardcoded recipients with configurable recipients per environment/tenant policy.
  6. Add idempotency and locking around scan-result processing to avoid double handling.
  7. Add locking/backoff semantics for Odoo retry command to avoid overlapping retries.
  8. 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

  1. Keep token validation and expiry checks for portal links.
  2. Remove fallback from hash to sequential OrderId in public URLs.
  3. Store file names safely and prevent path traversal.
  4. Restrict MIME acceptance with server-side sniffing, not client mime only.
  5. Add audit log for upload, download, delete, quarantine, and Odoo upload actions.

Operational Requirements

  1. Define scan polling frequency and max batch size per environment.
  2. Track metrics:
  3. uploads total
  4. scans pending/scanning/clean/infected/error
  5. Odoo upload success/failure
  6. infected notifications pending/failed
  7. Alert on:
  8. infected detections
  9. repeated Odoo upload failures
  10. 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

  1. Preserve orders.documentPortalToken and orders.documentPortalExpiry values.
  2. Preserve all order_documents rows and files.
  3. Preserve infected_document_notifications history.
  4. Backfill any legacy missing timestamps or statuses with explicit migration rules.
  5. If quarantine policy changes, migrate existing infected records accordingly.

Open Decisions

  1. Should portal links remain valid for 30 days or become configurable?
  2. Should unsafe files be hard-blocked for all users, including admins?
  3. Should infected files be hard-deleted or moved to a dedicated quarantine store?
  4. Should recipient policy for infected alerts be global, per tenant, or per environment?
  5. Should scan and Odoo upload run in synchronous cron commands or queue workers?