Orders & fulfilment
Returns & Exchanges (Self-Service RMA Portal)
Generated from docs/plan/core/returns-exchanges/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Returns are a customer service burden — merchants manually handle RMA requests via email. This feature provides a self-service RMA portal so customers create return requests without calling support.
The problem
Every return starts with an email asking ‘How do I return this?’. I have to manually issue RMA numbers, print labels, and track returns in a spreadsheet. A self-service portal would cut support tickets by 40%.
What it does
- Add returns collection: {id, order_id, order_item_id, reason (defective|wrong_size|wrong_color|damaged|not_as_described|other), status (requested|approved|shipped|received|inspected|refunded|completed), created_at, created_by_customer_id, notes}
- Storefront feature: customer clicks ‘Return Item’ from order detail, selects reason, submits return request
- Staff approves/denies returns from admin panel with optional comments
- Core generates RMA number and includes in approval email to customer
- Approved return shows: RMA number, return shipping label (if enabled), instructions, deadline to ship back
- Staff marks return as ‘received’, inspects items, then issues refund
- Track refund status separately: approved → issued → completed
- Display return history in customer account (storefront) and order detail (admin)
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Automatic refund issuance — that is payment processing, requires verification of received items; staff must approve before refund triggers
- Return shipping label generation from carriers (FedEx, UPS) — that is shipping-labels-carrier-sync (paid module); core only stores label URL if provided
- Advanced return logic (exchange-first, store-credit routing, restocking fees) — that is exchange-first-returns (paid module); core only handles basic return state
Data model
returns collection: {id, order_id, order_item_id, reason, status, created_at, created_by_customer_id, approved_at, approved_by_user_id, rma_number, shipping_label_url, refund_amount_minor, refund_status}. Migration: new collection. orders.return_status field (none, pending_approval, approved, returned, completed).
API
- POST /api/customers/:customerId/returns — create return request
- GET /api/customers/:customerId/orders/:orderId/returns — list returns for order
- GET /api/admin/returns — list all return requests with status
- PATCH /api/admin/returns/:returnId/status — update return status (approve, deny, mark received)
- POST /api/admin/returns/:returnId/refund — issue refund
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Admin panel ‘Returns’ section shows: (1) pending requests with customer name, order #, items, reason; (2) action buttons ‘Approve’ / ‘Deny’; (3) approved returns with RMA number, shipping label link (if available), refund button; (4) completed returns with refund amount and date.
The seam — why this is core
Core owns return entity, RMA numbering, approval workflow, and basic state machine (requested → approved → received → refunded). Paid module (exchange-first-returns) owns exchange-first logic, store-credit routing, and advanced rules. Paid module (shipping-labels-carrier-sync) owns return shipping labels.
Core owns return status machine, refund state, and RMA interface; paid modules own advanced exchange logic, store-credit routing, and carrier label generation
Dependencies
- Existing orders, order_items, customers collections
- email layer (to send RMA approval/denial emails)
- Existing storefront (to show ‘Return Item’ button)
- payments system (to issue refunds)
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- Customer submits return request for damaged item; confirm returns collection stores reason=‘damaged’, status=‘requested’
- Staff approves return; confirm RMA number is generated and return status changes to ‘approved’
- Verify approval email is sent to customer with RMA number and return instructions
- Staff marks return as ‘received’; confirm status=‘received’
- Staff issues refund; confirm refund is processed and status=‘completed’
- Verify return appears in customer account dashboard with RMA number and status
Risks
Fraudulent returns (customer claims item is defective when it’s not) can be hard to prevent without photo verification. Implement optional photo upload on return request for staff review. Refund issuance without proper verification can cause chargebacks; enforce ‘received’ status before refund can be issued.
Commercial context
| Suggested price | free (core) |
| Rival anchor | 6 apps (ReturnGO, Return Prime, Redo, Xcotton, Rich Returns, Yanet); all freemium with core returns tracking free |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and ../../AI-GUIDE.md for how to work on this repository.