updates on the reverse logistics
This commit is contained in:
293
docs/reverse-logistics-rider-app.md
Normal file
293
docs/reverse-logistics-rider-app.md
Normal 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`.*
|
||||
Reference in New Issue
Block a user