DEV Community

hans
hans

Posted on • Originally published at hanswys.hashnode.dev

Bridging the Tracking Gap: Implementing Manual Returns for Retailers and Customers

In the world of e-commerce returns, "Mail Returns" (where a customer ships a parcel themselves) are often a black hole. Because the system doesn't generate a label, there is no automatic tracking. No one knows where the package is, and the "he-said-she-said" over shipping proof becomes a support nightmare.

We recently tackled this by building a manual tracking flow that serves two distinct masters: Retailers needing oversight and Customers needing peace of mind. Here is how we built a unified tracking system across two different UIs with a shared backend.


The Problem: The "Mail Return" Black Hole

When a return is marked as Mail, the customer uses their own shipping method (e.g., national post or a local courier).

  • For Staff: They can't see when a return is coming or if it's even been sent.

  • For Customers: They have no way to "prove" they shipped it within the platform.

  • The Technical Gap: Since no label is generated by our system, no tracking data is auto-filled.

We needed a way for both parties to manually input Tracking Numbers and Courier Names into a shared source of truth.


The Architecture: One Logic, Two Gates

Instead of building two separate tracking services, we opted for a shared form object pattern. This ensures that whether a retailer updates a number or a customer does it, the validation and persistence logic remain identical.

1. The Backend Foundation

We leveraged a Shipment::UpdateForm object to handle the heavy lifting. This kept our controllers thin and our models clean.

Feature

Implementation Detail

Endpoints

PATCH .../customer/shipment/tracking

PUT .../retailer/shipment_tracking

Validation

Max 64 chars for tracking; Max 128 for courier; Alphanumeric only.

State Guard

Only allows updates if the return is in pending or pending_action.

Data Model

Updates tracking_number and custom_courier_name on the Shipment model.

Pro-Tip: Avoid heavy model callbacks. By performing the update within the Form Object’s submit method, we ensured that side effects (like invalidating cache) only happened when the form was actually valid.


The Frontend Implementation

Retailer Experience (React + Polaris)

In the retailer dashboard, we focused on speed and clarity for support agents.

  • Contextual UI: If the shipping method is "Mail," an "Add Tracking Number" button appears.

  • The Modal Flow: Clicking the button triggers a Polaris modal that pre-fills existing data if the agent is editing rather than adding.

  • Immediate Feedback: Upon saving, the app invalidates the return order query, triggering an instant UI refresh to show the new "Track" button (which links to the external tracking URL).

Customer Experience (Remix + React)

The customer-facing side needed to be more "walk-through" style. We integrated this directly into the Return Summary page.

  • Progressive Disclosure: We show the "Tracking Number" and "Courier" labels even if empty to signal that this information is expected.

  • Inline Editing: Instead of a modal, we used an inline form. The "Save" button remains disabled until both fields are filled to ensure data integrity.

  • I18n Ready: Since we operate globally, all labels (add_tracking_button_label, etc.) are served via the API to support multi-language storefronts.


Key Technical Takeaways

  1. Shared Validation is King: By using a single Form Object, we prevented "data drift" where a retailer might be able to enter a character that a customer’s UI would reject.

  2. Status-Awareness: Don't let users edit tracking after a return is "Completed" or "Cancelled." Hard-coding these guards into the service layer prevents API abuse.

  3. The "Track" Button Logic: We implemented a fallback mechanism. The UI displays the custom_courier_name if present, but falls back to the system's courierName for legacy data, ensuring the "Track Shipment" link never breaks.

Final Thoughts

Manual tracking isn't just about adding a text field; it's about creating a reliable audit trail. By building a unified backend and tailoring the UI to the specific needs of retailers and customers, we turned a "black hole" into a transparent part of the return lifecycle.

Top comments (0)