294 lines
11 KiB
Markdown
294 lines
11 KiB
Markdown
# 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`.*
|