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/findPOST /returns/findGET /returns/{orderRef}/{token}POST /returns/{orderRef}/{token}GET /returns/deniedGET /returns/received
Services¶
ReturnEligibilityServiceReturnSubmissionServiceReturnAttachmentServiceReturnNotificationService
Persistence¶
Use explicit entities:
- ReturnRequest
- ReturnRequestItem
- ReturnAttachment (new, instead of JSON file list)
Add audit timestamps:
- createdAt
- updatedAt
- submittedAt
- optional reviewedAt
Security And Access¶
- Keep logged-in ownership guard.
- For guest flow, use signed short-lived access token instead of raw order id + reference.
- Log denied attempts with reason code and requester context.
- 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¶
- Keep existing
order_returnandorder_return_itemrecords. - Backfill status as
submittedfor existing rows. - If attachment table is introduced, migrate
order_return.imagesJSON entries to rows. - Ensure one-to-one relation with order is preserved during migration.
Operational Notes¶
- Add monitoring for return submit errors and attachment failures.
- Add dashboard metric: submitted returns per day, denied reasons distribution.
- Add alert when notification sending fails.
Open Decisions¶
- Should return window remain fixed at 14 days or become tenant-configurable?
- Should guest return flow remain invoice/customer-id based, or require signed link from invoices only?
- Should a single order support multiple return requests over time, or keep one-request-per-order?
- Should returns feed ERP/Odoo directly in this phase, or remain email/admin workflow first?