Files
doormile_backend/docs/reverse-logistics-rider-app.md

11 KiB

Reverse logistics (Return to sender): rider app integration

This is the backend contract the Miler (rider) app needs in order to support returns, also called RTO ("return to origin"). It lists what the server sends, what the app should show, the one new endpoint, and the order to roll it out in.

Base URL https://api.doormile.com/api/v1. Every endpoint here uses the normal rider login (Authorization: Bearer <miler token>).

Summary for the app team. A parcel can now be sent back to its sender instead of being delivered. While that is happening, the parcel's consignment status is RTO_Initiated, but the booking status does not change. Today the app decides what to show from the booking status, so a parcel being returned still looks deliverable. Tapping Deliver or Skip then returns a 400. The app must read the per-parcel fields below and show a "Return to sender" stop instead.


1. What happens to a parcel

Collected_By_Miler / Out_for_Delivery
        │  ops press "Return to sender" in the console,
        │  or automatically after 3 failed delivery attempts (skips)
        ▼
  RTO_Initiated ──── ops "Re-attempt delivery" ───▶ back to Out_for_Delivery
        │                                            (or wherever it was)
        │  rider hands it back to the sender  ← NEW: POST …/return-complete
        │  (or ops press "Mark returned")
        ▼
  Returned_to_Sender   (final; the rider's job on it is closed)
  • Where it goes: back to the sender, which is the parcel's pickup point. The server sends the coordinates (return_to, see §3). The app never chooses the destination.
  • Who starts it: ops, from the console, or the server automatically on the 3rd failed attempt. The rider never starts a return.
  • Who finishes it: the rider, through the new endpoint once the feature flag is on (§6), or ops from the console.

Wire values (spell exactly like this)

Thing Value
Consignment status while returning RTO_Initiated
Consignment status once returned Returned_to_Sender
next_action while returning return_to_sender (only when the flag is on, §6)
next_action once returned none
return_to.type sender
Push data.type rto

2. The feature flag (server side)

MILER_RTO_FLOW_ENABLED is an environment variable on the server, off by default. It is read on every request.

flag off (today) flag on (after your release)
next_action for a parcel being returned none return_to_sender
can_return false true
POST /miler/consignments/:id/return-complete 403 RTO_FLOW_DISABLED works
Who closes the return ops, in the console the rider (or ops)

returning, return_reason and return_to are sent in both states, so the app can show the return as soon as your build ships. That is before the flag goes on.


3. Reading a parcel: GET /miler/consignments/:consignmentid

Unchanged fields stay as they were. Four fields are new and are additive, so older builds ignore them.

Field Type Meaning
returning bool true while the parcel is being returned (status == "RTO_Initiated").
can_return bool true when the rider may finish the return in the app (returning and flag on). Show the "Returned to sender" button only when this is true.
return_reason string Why it is being returned, e.g. "Receiver refused: gate locked". "" when not returning.
return_to object | null Where to take it. null when not returning.

return_to:

{ "type": "sender", "latitude": 11.0168, "longitude": 76.9558, "pincode": "641001" }

Example response for a parcel being returned (flag on):

{
  "success": true,
  "data": {
    "consignmentid": 9103,
    "trackingno": "DMX09103",
    "status": "RTO_Initiated",
    "attemptcount": 0,
    "next_action": "return_to_sender",
    "returning": true,
    "can_return": true,
    "return_reason": "Address not found: No such door number",
    "return_to": { "type": "sender", "latitude": 11.0168, "longitude": 76.9558, "pincode": "641001" },
    "can_deliver": false,
    "can_skip": false,
    "can_start_delivery": false,
    "can_inward_at_hub": false,
    "delivered": false,
    "out_for_delivery": false,
    "collected": false,
    "paymentmode": "",
    "codamount": 0,
    "codcollected": 0,
    "next_hub": null,
    "inwardedat": null
  }
}

With the flag off, the same parcel has "next_action": "none" and "can_return": false. Everything else is the same.

Note that can_deliver, can_skip and can_start_delivery are all false while returning. If the app already uses these flags to enable its buttons, the buttons disable themselves correctly.


4. The queue: GET /miler/bookings

Each stop already carries consignmentid, consignmentstatus, trackingno, next_action and next_hub. Nothing new is added here. A stop being returned shows:

  • consignmentstatus: "RTO_Initiated"
  • next_action: "return_to_sender" (flag on) or "none" (flag off)
  • the booking status unchanged (e.g. Converted_To_Consignment)

This is the important app change: ApiConfig.legacyStatusFromNew maps Converted_To_Consignment to picked. A parcel being returned therefore renders as a normal delivery today. Before mapping a stop to a delivery card, check consignmentstatus / next_action:

consignmentstatus next_action Show
RTO_Initiated return_to_sender Return to sender card: navigate to return_to, "Returned to sender" button
RTO_Initiated none Being returned: no Deliver/Skip buttons; text such as "Return to sender. Ops will close this."
Returned_to_Sender none Done: move to history like a delivered stop

To get return_to and return_reason for a stop, read GET /miler/consignments/:consignmentid. A push (below) is also a good moment to refresh.


5. Finishing a return: POST /miler/consignments/:id/return-complete (NEW)

The rider has handed the parcel back to the sender.

Headers: Authorization: Bearer <token>, Content-Type: application/json, and Idempotency-Key: <uuid> (recommended; see below).

Request:

{
  "lat": 11.0168,
  "lon": 76.9558,
  "receivedby": "Acme kitchen manager",
  "photourl": "https://…/proof.jpg"
}
Field Required Notes
lat, lon send them Rider's position at handover. Stored in the parcel history.
receivedby optional Who at the sender took it back.
photourl optional Proof photo. Upload it the same way as delivery proof (POST /miler/uploads/sign, then use the URL).

Success 200:

{ "success": true, "data": { "consignmentid": 9103, "status": "Returned_to_Sender", "next_action": "none" } }

What the server does on success:

  • parcel → Returned_to_Sender, with the return time stored;
  • a history row: Returned to sender by rider at (lat, lon), received by …, photo …;
  • the rider's assignment on that booking → Completed.

The rider's job on that parcel is then closed, so it no longer blocks End duty.

Errors. Codes follow the existing miler format, {"success": false, "code": "...", "message": "..."}. Plain 400/404/500 responses have no code and only carry message.

HTTP code When App should
403 RTO_FLOW_DISABLED The server flag is off Hide the button. This should not happen if you check can_return.
400 INVALID_STATE Parcel is not being returned (e.g. ops re-attempted it, or it was delivered) Refresh the parcel and show its new state
404 — (message: "consignment not found") No such parcel Refresh the queue
404 CONSIGNMENT_NOT_ASSIGNED Parcel isn't on this rider's bookings Refresh the queue
400 — Bad id or bad JSON body Bug in the app
500 — Server error, nothing was saved Retry with the same Idempotency-Key

Retries are safe. With the same Idempotency-Key, a successful response is replayed for 24 h. Even without the key, a second call on a parcel that is already Returned_to_Sender returns 200 and changes nothing.


6. Skip responses change on the 3rd attempt

POST /miler/consignments/:id/skip is unchanged in what it accepts. The response status can now be RTO_Initiated:

{ "success": true, "data": { "consignmentid": 9102, "attemptcount": 3, "status": "RTO_Initiated" } }

That is the server starting the return automatically, by default on the 3rd failed attempt (server setting RTO_AUTO_AFTER_ATTEMPTS; ops may set it to 0 to turn this off). After a skip, use the returned status. If it is RTO_Initiated, switch the stop to the return card (§4) instead of keeping it as a delivery to retry.

Also new: skip now works for the 2nd, 3rd … orders of a multi-drop pickup. It used to answer CONSIGNMENT_NOT_ASSIGNED for every order after the first.


7. Push notification

When ops start a return (or it starts automatically), the rider holding the parcel gets:

title Return parcel to sender
body Parcel DMX09103 is being returned to the sender. Do not attempt delivery.
data { "type": "rto", "consignmentid": "9103" } (both strings)

On type == "rto": refresh that consignment (§3) and the queue (§4). A rider who already handed the parcel over at a base is not notified.


8. Rollout order

  1. Backend deployed with the flag off. Ops start and close returns from the console; riders get the push. Until step 3, a return can leave a rider unable to end duty until ops press "Mark returned". To avoid that, ops may deploy with RTO_AUTO_AFTER_ATTEMPTS=0.
  2. App release: §4 (card per consignmentstatus / next_action), §3 fields, §5 button behind can_return, §6 skip handling, §7 push.
  3. Flag on (MILER_RTO_FLOW_ENABLED=true) once most riders have the new build. Riders finish their own returns, and automatic returns can be turned on.

Old builds keep working at every step. The new fields are additive, and with the flag off nothing new is required from the app.


9. App test checklist

Use a staging backend with MILER_RTO_FLOW_ENABLED=true.

  • Ops start a return on a parcel the rider is carrying → push arrives → the stop turns into a Return to sender card with the reason; Deliver and Skip are gone.
  • Navigation goes to return_to (the sender), not to the receiver.
  • "Returned to sender" (with photo and receiver name) → 200 → the stop moves to history → End duty works.
  • Tap it twice or with no network, then retry → no error, one return.
  • Skip the same parcel 3 times → the 3rd response has status: RTO_Initiated → the card switches to return.
  • Ops press "Re-attempt delivery" while the card is open → button tap gets 400 INVALID_STATE → the app refreshes and shows the delivery again.
  • Flag off: the card shows "Being returned", with no button and no 403 shown to the rider.
  • Multi-drop pickup: skip and return work on the 2nd and 3rd order.

Server code: controllers/consignmentReturn.go. The full plan, decisions and test record are in krow_talent_app/docs/reverse-logistics-plan.md.