Skip to content

Webshop - Returns / RMA

Overview

This document specifies the missing Returns/RMA behavior that is currently in legacy code but not yet documented in the migration set.

Checklist coverage: - WF-023 Return/RMA find + submit + denied + received flows - OR-003 Return request domain model - OR-004 Return request mail behavior

Primary legacy evidence: - _core/config/pages.xml - _core/controller/classes/atx/orders/ReturnController.php - _core/view/classes/atx/orders/ReturnView.php - _core/model/classes/atx/orders/ReturnRequest.php - _core/model/classes/atx/orders/ReturnRequestItem.php - _core/model/classes/atx/orders/ReturnRequestMail.php - _core/config/doctrine/orders.ReturnRequest.orm.xml - _core/config/doctrine/orders.ReturnRequestItem.orm.xml - _core/model/classes/atx/orders/Order.php

Legacy User Flow (As-Is)

Entry Pages

Page key Purpose
return-find Public lookup form for invoice + customer reference
return Return submission form
return-denied Denial screen with reason code
return-received Confirmation screen after successful submit

Step 1: Find Return (return-find)

Input fields: - invoiceNumberPrefix - invoiceNumber - customerId

Behavior: 1. Prefix is limited to current year and previous year (VKF/{yy}/). 2. Invoice lookup by invoice number + customer id. 3. Order lookup by invoice reference. 4. If order exists and order.hasReturnableItems() == true, redirect to return with order id + reference. 5. Otherwise show form error (Order kon niet worden gevonden.).

Step 2: Eligibility Gates (return)

The return page denies access when one of these checks fails:

Reason code Condition
0 Missing route params (id, ref)
1 Order not found or reference mismatch
2 Order has no returnable items
3 Logged-in user is not owner of order (except Atraxion user)
4 Order already has a return request

Returnability in legacy currently means: - Invoice must exist - Invoice date must be within 14 days (Order::hasReturnableItems())

Step 3: Submit Return

Form behavior: - Prefills company/contact/address from logged-in customer when available. - Candidate return lines are only physical order items (OrderArticle::canReturn()). - Reason options: - ordered wrong item - received wrong item - damaged on delivery (only enabled when invoice age <= 3 days)

Validation in controller: - Required: businessName, contactName, contactEmail - Required pickup address: street/postcode/city/country - Required: at least one selected items[] - Required: GDPR consent (agreeTerms)

Upload behavior: - Files uploaded from images[] - Stored under /images/return-requests/{orderId} - Image width reduced to max 1000px - Stored image paths persisted as JSON array in order_return.images

On successful submit: 1. Persist ReturnRequest + item selections. 2. Send internal mail. 3. Redirect to return-received.

Step 4: Mail Notification

ReturnRequestMail behavior: - Disabled in dev environment. - Recipient: retour@atraxion.com. - Subject/body rendered from inline Twig template. - Includes contact fields, order info, selected item lines, and image attachments.

Legacy Data Model (As-Is)

order_return

Main return request aggregate: - id - order_id (one-to-one with order) - business_id (customer) - business/contact fields - pickup address (embedded) - comment - images (json)

order_return_item

Return line records: - id - return_id - item (OrderArticle FK) - quantity - reason

Order relationship

Legacy ORM mappings show one-to-one links on both sides: - order_return.order_id (return -> order) - orders.return_id (order -> return)

Migration should consolidate this to one canonical FK direction.

Target Migration Specification (Symfony)

Scope

In-scope for Webshop island: - Public return lookup and submission flow - Denied + received pages - Return aggregate persistence - Internal notification dispatch

Out-of-scope for this document: - Post-submission warehouse workflow and refund accounting - Document malware scanning (covered in section 14 item 2)

Domain Rules To Preserve

Mandatory parity with legacy behavior: 1. Return window: 14 days from invoice date. 2. Return lines only for physical products. 3. One return request per order. 4. Damaged-on-delivery reason only within 3 days. 5. Non-owner logged-in customers cannot return someone else’s order.

Required Improvements In Migration

The new implementation should fix legacy weak points: 1. Validate at least one selected line with quantity > 0. 2. Require a reason for each selected line with quantity > 0. 3. Enforce attachment policy (count, size, allowed mime types) server-side. 4. Sanitize stored filenames and avoid trusting raw client filenames. 5. Add explicit return status lifecycle instead of implicit "exists = submitted".

Recommended statuses: - submitted - under_review - approved - rejected - received - closed

Suggested Application Design

Web Routes

  • GET /returns/find
  • POST /returns/find
  • GET /returns/{orderRef}/{token}
  • POST /returns/{orderRef}/{token}
  • GET /returns/denied
  • GET /returns/received

Services

  • ReturnEligibilityService
  • ReturnSubmissionService
  • ReturnAttachmentService
  • ReturnNotificationService

Persistence

Use explicit entities: - ReturnRequest - ReturnRequestItem - ReturnAttachment (new, instead of JSON file list)

Add audit timestamps: - createdAt - updatedAt - submittedAt - optional reviewedAt

Security And Access

  1. Keep logged-in ownership guard.
  2. For guest flow, use signed short-lived access token instead of raw order id + reference.
  3. Log denied attempts with reason code and requester context.
  4. Rate-limit find endpoint to reduce invoice/customer-id probing.

Acceptance Scenarios (Gherkin)

Feature: Returns and RMA

  Scenario: Find return for eligible order
    Given invoice "VKF/26/12345" belongs to customer "10422"
    And the linked order has an invoice date within 14 days
    When I submit the return lookup form
    Then I should be redirected to the return submission page

  Scenario: Deny return when order is out of window
    Given invoice "VKF/26/10001" belongs to customer "10422"
    And the linked order invoice is older than 14 days
    When I open the return page
    Then I should see the denied page with reason code 2

  Scenario: Deny return when logged-in user is not owner
    Given I am logged in as customer "20000"
    And I open a return link for order owned by customer "10422"
    Then I should see the denied page with reason code 3

  Scenario: Submit return with valid data
    Given I am on an eligible return page
    And I select at least one physical item with quantity greater than 0
    And I provide required pickup/contact fields
    And I accept GDPR terms
    When I submit the form
    Then a return request should be persisted
    And I should be redirected to the return received page

  Scenario: Reject submit without selected quantities
    Given I am on an eligible return page
    When I submit with all item quantities set to 0
    Then validation should fail
    And no return request should be persisted

  Scenario: Damaged on delivery reason expires after 3 days
    Given invoice age is 5 days
    When I open the return form
    Then "damaged on delivery" reason should not be selectable

  Scenario: Attachments are persisted securely
    Given I add image attachments to a return
    When I submit successfully
    Then attachments should be stored with sanitized names
    And attachment metadata should be linked to the return request

  Scenario: Internal notification is sent on submission
    Given a return request was submitted successfully
    When notification dispatch runs
    Then an internal return email should be sent with selected line details

Data Migration Notes

  1. Keep existing order_return and order_return_item records.
  2. Backfill status as submitted for existing rows.
  3. If attachment table is introduced, migrate order_return.images JSON entries to rows.
  4. Ensure one-to-one relation with order is preserved during migration.

Operational Notes

  1. Add monitoring for return submit errors and attachment failures.
  2. Add dashboard metric: submitted returns per day, denied reasons distribution.
  3. Add alert when notification sending fails.

Open Decisions

  1. Should return window remain fixed at 14 days or become tenant-configurable?
  2. Should guest return flow remain invoice/customer-id based, or require signed link from invoices only?
  3. Should a single order support multiple return requests over time, or keep one-request-per-order?
  4. Should returns feed ERP/Odoo directly in this phase, or remain email/admin workflow first?