Back to Journal
logisticsen

RMA: reverse logistics flow

Self-service portal, pre-paid label, receiving inspection, refund/exchange. The hardest part is the cross-warehouse return: which warehouse keeps the unit?

Return merchandise authorization (RMA) is the feature everyone postpones until "I'd like to return this" emails start multiplying in the support queue. For a mid-sized B2C webshop return rates run 4-12%; for fashion webshops they can climb to 25%. Here is how we built the RMA flow inside Netorigo Logistics.

Customer self-service portal

The customer opens their order and clicks "start a return", landing on a four-step form:

  1. Which items? (multi-select from the order's SKUs).
  2. Why? (dropdown: defective, different than expected, did not fit, duplicate order, other).
  3. Condition? (unopened, opened-used, damaged).
  4. Photo upload (optional, mandatory for "damaged" and "defective").

The form writes a return_requests row in pending_approval state. Auto-approval is per-tenant policy: if the SKU has return_policy.auto_approve = true, the purchase is younger than 30 days, and the value is below HUF 50,000, the return is approved immediately. Otherwise the partner's support team approves manually within 24-48 hours.

Pre-paid label generation

On approval we automatically generate a pre-paid return label. The carrier is chosen by partner config (mostly GLS or MPL in Hungary), and the cost is booked to the return_label_cost ledger. The customer receives a PDF label by email and drops the parcel at a nearby Foxpost locker or post office.

Receiving inspection at the warehouse

Returned parcels land in a special RMA receiving wave (not the general receiving wave). The picker opens the return_request on the PWA, scans the incoming SKUs, and rates each one:

  • as_new: unopened, original packaging. Goes back to the shelf as available stock.
  • usable: opened but resellable. Becomes outlet stock.
  • damaged: damaged. Becomes quarantine stock, supervisor decides next.
  • not_returnable: customer sent back the wrong thing, or something is missing. The return_request is marked rejected, and support contacts the customer again.

Every rating requires a photo in return_inspection_photos. In a dispute (customer: "I sent it in good condition"; warehouse: "it arrived broken") the photo is the source of truth.

Refund/exchange decision

After inspection the return_request moves to awaiting_resolution. If the customer asked for a refund, the Finance module's credit_note is generated automatically (via Logistics' REST API call), and the payment is reversed through the Payment module's refund action onto the original payment method. If they asked for an exchange, the Logistics module generates a new order with the original payment.

Restocking-fee logic is configurable per tenant and per SKU. One of our electronics partners charges a 15% restocking fee on every opened return because the outlet channel margin is thin. They deliberately turned off auto-approve, so support decides case by case.

The hardest part: cross-warehouse returns

The customer bought from warehouse 18 and shipped the return to warehouse 33 (closer to them). Does warehouse 18 receive the unit back logically? Not necessarily. If the customer chose store credit (not a refund), and warehouse 33's inventory lookup is higher priority, the unit stays in warehouse 33. If they chose a refund, a cross_stock_request is generated automatically toward warehouse 18 and rides the next inter-warehouse wave. This logic lives in WarehouseReceivingPolicyService.

Finance integration

The credit_note generation calls the Finance module's /api/v1/credit-notes endpoint with a tenant-aware JWT. The response lands in return_requests.credit_note_id, so audits can drill across both modules. For NAV (Hungarian tax authority) compliance, the credit-note total must equal the original invoice total - if we issue a partial refund (not all six lines came back), the credit note covers only the returned lines.