# 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 `). > **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`: ```json { "type": "sender", "latitude": 11.0168, "longitude": 76.9558, "pincode": "641001" } ``` Example response for a parcel being returned (flag on): ```json { "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 `, `Content-Type: application/json`, and **`Idempotency-Key: `** (recommended; see below). Request: ```json { "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`: ```json { "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`**: ```json { "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`.*