updates on the reverse logistics

This commit is contained in:
2026-10-06 11:05:03 +05:30
parent cb3108ffdb
commit 220e934045
10 changed files with 1541 additions and 8 deletions

View File

@@ -0,0 +1,293 @@
# 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`:
```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 <token>`, `Content-Type: application/json`, and
**`Idempotency-Key: <uuid>`** (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`.*