# NexGate Shipping Service — Development guide

**Status:** design agreed in discussion (2026-10-12), **not built yet**. Built on the Location Service
(wards, shop locations, user addresses, frozen places — see `commerce_logistics_changes.md`).
Source specs: doc-hub *Logistics Service* book (location spec + commerce/location/logistics master guide).
Where this document and the master guide differ, **this document wins** (it records our decisions).

Rule of thumb for everything below: **simple over clever.** Shipping prices are placeholders (0) until this
service is built; build what is decided, keep a visible list of what is not.

---

## 1. Principles

1. **Two ways to get an order: PICKUP or DELIVERY**, chosen per shop. The shipping service owns both.
2. **One shop's order = one parcel.** A cart with three shops = three parcels, three fulfillments.
3. **User addresses are places only** (pin, label, directions). Order data — who receives, notes — lives on the
   order, never in the address book.
4. **Everything about places is frozen** on the order when the checkout session is created. Later edits to a shop
   location, an address or a price never change an existing order.
5. **Money sits in escrow** and moves on proof (codes), never on trust.
6. **Shippers describe their own coverage, prices and times.** NexGate never forces one model.
7. **No manual approval queues.** Trust comes from documents, codes, reviews and automatic rules.
8. **Orders are accepted 24/7; timing is honest.** Closed hours delay work, never the sale.
9. **Pickup and delivery are designed together.** Every feature must say what it does for a pickup order.

---

## 2. Big picture

```
MESSAGES (everyone talks only to NexGate; the SHIPPING AGREEMENT is the deal they all sign — §4.8)
  buyer   ── estimation / quotation / agreement / pay ──►┐
  seller  ── accept / ready / verify pickup code ───────►├── NexGate ── escrow: items + legs
  shipper ◄── offers ──  ── accept / status / code ─────►┘   (native: app · integrated: API)

PARCEL (physical, with the code that unlocks each handover)
  pickup:    shop ──[buyer's code]──► buyer
  delivery:  shop ──[pickup code]──► shipper ──[delivery code]──► buyer
  chain:     shop ──► shipper 1 ──[handover code]──► shipper 2 ──► … ──► buyer
```

### Package layout (new top-level `shipping_service/`, next to `location_service/`)

| Package | Owns |
|---|---|
| `fulfillment/` | one record per shop order: PICKUP or DELIVERY, frozen places, receiver, note, status, codes, timers. (Replaces the Step-4 `e_commerce/fulfillment` JSON — the order links to this record.) |
| `business/` | native shippers ("delivery businesses"): KYC, Service Setup, coverage, pricing, hours, stations, reviews |
| `providers/` | integrated shippers: one adapter per provider |
| `engine/` | estimation + quotations: coverage, pricing, ETA, chains, ranking — native and integrated together |
| `jobs/` | offers, accept / reject, reassignment, status steps, handovers |

Shared pieces used across: `opening_hours` (shops, businesses, stations), the area picker and frozen places
from `location_service`.

---

## 3. Fulfillment: pickup and delivery

### 3.1 Pickup
```
placed → accepted → ready → collected → completed
exits: cancelled (buyer before accept, or seller) · expired (14 days)
```
- **accepted** — the seller took the order (the seller's signature, §4.8).
- **ready** — one tap, button **"Packed & ready"** (packing and ready are the same moment; not split) → buyer is
  told "ready at Ubungo, Gate B" (directions, shop phone).
- **collected** — the **seller enters the buyer's 4-digit code** at the counter.
- **completed** — the buyer tapped **"✓ It's what I ordered"** → money released.
- **Pickup time (optional)** — after Ready the buyer sees **"When will you come?"**: a date + time picker offering
  only the **shop's opening hours** inside the window. The seller sees it on the order ("Buyer coming Thu 15:00");
  the buyer gets a reminder that morning; time passed and not collected → the buyer is asked to pick a new time.
  Changeable any time, **not binding**, a field on the order (not a status); it **does not move the window**.
- **Collection window 14 days, counted from Ready**: reminders on the Ready day, day 7, day 12 and day 13 ("last day tomorrow");
  not collected by day 14 → auto-cancel, full refund, stock back.
- **The seller can cancel at once** while only the buyer and the seller are involved — no waiting for the window;
  they talk by phone (damaged, out of stock, the buyer asked…). No reason list: one **pop-up** —
  *"I have already informed the buyer about this cancellation"* → confirm → cancelled, buyer notified, **full
  refund**, stock back (the seller fixes stock by hand if the item is damaged).
  - **Pickup**: any time before *collected*.
  - **Delivery**: only **before Ready** — no shipper has an offer yet (heads-ups and Upcoming cards are not
    commitments; they disappear). After Ready, shippers are involved → open (§15 #13).

### 3.2 Delivery (door-to-door)
`placed → accepted → ready → shipper accepted → heading to the shop → picked up → on the way → delivered → completed`
(the same `accepted` / `ready` / `completed` as pickup; "ready" is when the offers go out)

**Heading to the shop** — the shipper taps it when they set out. It marks when cancelling the shipping starts
to cost the buyer (§3.4a).

### 3.3 Delivery (station)
`placed → accepted → ready → shipper accepted → heading to the shop → dropped at station → in transit → at destination station → collected → completed`
(the parcel always starts with a collection at the shop — by a door shipper, or by the station business itself when
it collects from shops, §7.2a)
(the buyer collects at the station; station staff enter the buyer's code)

### 3.4 Switching (buyer only, many times allowed within the rules)
| From → to | Allowed | Money |
|---|---|---|
| Pickup → delivery | any time **before the buyer collects** | fresh quotes at today's prices; buyer pays the delivery fee from the wallet into new leg part(s); if the order is already *ready*, the offer goes to the shipper at once |
| Delivery → pickup | **before pickup** (same rules as cancelling the shipping, §3.4a) | leg part(s) refunded, minus the wasted-trip fee if a shipper is already heading to the shop |

### 3.4a Cancelling (buyer)
| Situation | Buyer can |
|---|---|
| **Before the seller accepts** | **cancel the whole order** — full refund |
| **After the seller accepts, no shipper accepted yet** | **cancel the shipping** (keep the order): **pickup** (delivery refunded) or a **new delivery proposal** (new quotation → new agreement; pay or get back the difference) |
| **A shipper accepted, not heading to the shop yet** | **cancel the shipping** → pickup or a new proposal; **free** (like Bolt); the shipper is released and told (no reliability effect on them) |
| **A shipper is heading to the shop** | **cancel the shipping** with a **wasted-trip fee** paid by the buyer: native = **50% of that leg's cost**, Bolt = Bolt's own cancellation charge |
| **Parcel picked up** | no cancelling the shipping — only a **destination change** (§4.8) or, in a chain, **stop at the next meeting point** (legs not started are refunded) |
| **Chain failed** — a shipper didn't accept and no replacement was found, or the buyer declined the dearer proposal | drop the shipping → **pickup**, a **new delivery proposal**, **end the chain earlier** — or **cancel the whole order** (full refund, stock back; not the buyer's fault) |

Rule: **whoever causes the cost pays it.** Switching delivery → pickup after the seller signed does **not** need
the seller to sign again (they agreed to hand the order over; who collects is the buyer's choice) — the seller is told.

### 3.5 What every fulfillment record holds
type · pickup place (the shop, frozen) · drop-off place (delivery, frozen) · receiver (name + phone; for pickup =
who collects; default = the buyer) · **parcel number** (§11) · planned pickup time (pickup, optional) · note (this order only) · the agreement (legs, shippers, meeting points, costs, ETAs) · status ·
codes · who paid what (free shipping split) · timestamps of every step.

---

## 4. Choosing: estimation → quotation → agreement

### 4.1 Three stages
| Stage | What | Where | Can change? | Lifetime |
|---|---|---|---|---|
| **Shipping estimation** | a hint: `cost {from,to}`, `eta {from,to}` | product cards and lists | — | cached, not stored |
| **Shipping quotation** | the **proposal**: recommended chain (legs, shippers, meeting points, cost, ETA) + alternatives per leg | add-to-cart / buy-now sheet, the cart | **yes** — destination, shipper per leg, meeting point, pickup ↔ delivery | prices **held 5 days** while the parcel is unchanged |
| **Shipping agreement** | the deal | **created at checkout** from the quotation | editable on the checkout screen **until paid** | **locked once paid** for the order's whole life |

```
card "🚚 20,000 – 35,000 to Mbeya"          ← estimation
 └ sheet / cart: QUOTATION — buyer edits ("collect at Mbeya Bus Stand", "use Abood")
     └ checkout: AGREEMENT — still editable before paying
         └ paid: LOCKED — shippers bound to these prices and legs
```

**Quotation rules**
- Holds its prices **5 days** as long as the parcel is unchanged (same items, quantities, destination, choices).
- Any change → a **new quotation** at current prices (the buyer made the change and sees the new cost).
- Older than 5 days → refreshed at today's prices and shown in the cart **before** checkout.
- A shipper changing its price table never affects quotations it already gave, or agreements.

**Agreement rules**
- Made when the checkout session is created. Edits on the checkout screen (before paying) re-calculate it and
  re-check the wallet; the 30-minute session expiry does not restart.
- Once paid: **the price is locked**; the agreement still needs the seller's and every shipper's signature
  before the parcel moves. After payment it changes only through **agreement changes** (§4.8).

### 4.2 Destination (where the quotation ends)
The start is always **the shop where the product is**. The buyer picks the end — no saved address needed:
saved address · recent pin · "use my location" · **search a known place** (Google Places in the app, e.g.
"Mbeya bus stand") · drop a pin. All end as a **confirmed pin** that we resolve (Google only in the app; we never
store Google's text; the buyer may add their own label).

**Meeting-point recognition:** a destination pin within **1 km** of a station → the quotation offers
"🚌 Collect at <company>, <station>" (no last-mile leg) and "🚪 Deliver to this exact pin" (if covered).

### 4.3 Add-to-cart / buy-now sheet
Deliver to (header address, changeable) · quantity · **Pickup** or **Delivery** with the ⭐ quotation pre-selected
and "change" (edit legs / destination).

```json
POST /api/v1/e-commerce/cart/add
{
  "productId": "…", "quantity": 2,
  "fulfillment": { "type": "DELIVERY", "quotationId": "…" },   // the SHOP's choice
  "dropoff": { "addressId": "…" }                               // only if changed in the sheet: the CART's destination
}
```

### 4.4 Cart rules
- **The choice belongs to the shop**, not the item — items from one shop share it.
- **One destination per cart**; changing it re-quotes every shop.
- Cart endpoints: set the cart's destination · set / edit a shop's quotation · get options for a shop.
- **Checkout from the cart turns its quotations into agreements.** Buy-now does the same from the sheet.
- **Unsaved new pin**: held on the **cart** as its destination (pin + directions) for as long as the cart exists;
  becomes a **recent pin only after payment** (abandoned carts never add recents). "Save as" → saved address at once.
- **Default for a shop newly added to the cart:**

  | Buyer has… | Shop's parcel defaults to |
  |---|---|
  | a destination and a shipper covers it | **Delivery** with the ⭐ quotation |
  | a destination, no shipper covers it | **Pickup** + "Delivery not available to Tabata" |
  | no destination at all | **Pickup** + "Add a delivery address to see delivery options" |

### 4.5 Estimation
```json
POST /api/v1/shipping/estimations
{ "dropoff": { "addressId": "…" }, "productIds": ["…", "…"] }
→ [ { "productId": "…", "cost": { "from": 3500, "to": 7000 }, "eta": { "from": "30 min", "to": "2 h" },
      "freeDelivery": false, "pickupArea": "Ubungo, Ubungo, Dar es Salaam" } ]
```
`cost` range = cheapest → dearest of the *sensible* options (dominated / outliers removed). No shipper →
`cost: null` (card shows "Pickup only"). Cached shop-area → buyer-area (Redis ~1 h). The app asks with the
address in its header; list and feed responses are unchanged. Prices are always called **cost**
(`cost`, `originalCost` when a discount applies).

### 4.6 What the app keeps vs the server
**Rule:** anything that affects money or a promise stays on the server; anything that is only "where the user was"
can live in the app.

| The app may keep (no backend storage) | Why it's fine |
|---|---|
| Header "Deliver to" for card estimations | sent with every estimation request; estimations are only hints |
| Last destination used / last map position | convenience for the pin screen |
| A sheet in progress (before tapping Add) | nothing committed yet |
| Recently searched places | the search runs in the app (Google) |
| UI preferences (list / map, "See all") | personal display |
| An unsaved pin, to offer it again on the pin screen | never becomes a backend recent unless the order is paid |

| Must stay on the server | Why |
|---|---|
| The cart's destination + each shop's quotation | quotations **hold prices 5 days** — a promise; must work on any device |
| Agreements | bind shippers and money |
| Saved addresses, recent pins | shared across devices, used when ordering |

### 4.7 Multi-shipper per order
One parcel can travel through **up to 3 shippers** (legs), e.g.:
```
Leg 1 🚪 Kibuti Express  Mlimani City shop → Magufuli Terminal (Dar)   3,500   2–3 h
Leg 2 🚌 Shabiby         Magufuli Terminal → Mbeya Bus Stand          21,000   1 day
Leg 3 🚪 Mbeya Riders    Mbeya Bus Stand   → buyer's door              2,500   1–2 h
```
- **Meeting points**: legs connect only where both businesses hand over — a station business's stations, a door
  business's office, or a station inside a door business's coverage. Two points within **1 km** count as the same place.
- **Fewer legs are fine**; the chain ends at the door or at a station, as the buyer wants.
- The engine **always** builds chains next to single-shipper options; ranking decides.
- In the quotation the buyer can **stop early** (collect at a station), **swap one leg's shipper**, or **change a meeting point**.
- Pickup and money per leg: §10, §11, §12.

### 4.8 The shipping agreement — parties, signatures, changes

The agreement is **the deal every party signs**: buyer, seller and every shipper on the route. It is its own
entity and **controls every important action** on a parcel: one place decides whether a change is allowed, one
history records who proposed and who approved, and **money moves only on an agreement event** (no other code
path releases or refunds an escrow part).

**Signatures — each party already says yes somewhere in the flow:**

| Party | Signs when | Agrees to |
|---|---|---|
| **Buyer** | pays at checkout | items, destination, legs, cost |
| **Seller** | accepts the order (within 24 opening hours) | pack it and hand it over |
| **Each shipper** | accepts the offer at Ready | carry their leg (capacity — the price came from their own price table) |

The buyer can be charged a fixed price before the shippers sign because each shipper's price table is a
**standing offer**; a shipper's signature means "I'll carry it", not "I accept the price".

**States:**
```
DRAFT      created at checkout; the buyer can still edit
PENDING    the buyer paid → waiting for the seller, then the shippers
AGREED     everyone signed → binding on all; the parcel can move
DONE       buyer confirmed "✓ It's what I ordered" → all escrow parts released
CANCELLED  refunded as the cancel rules say (§3.4a)
```
- A shipper rejecting or timing out = **declining to sign** → that party is replaced (reassignment, §10a); the
  agreement stays PENDING.
- **"The parcel doesn't move until all legs accept"** = it doesn't move until AGREED.
- A shipper's **Upcoming** list (§6.6) = agreements waiting for that shipper's signature.
- A bulky mismatch (§8.4) = a shipper **withdrawing** their signature before pickup for a reason that is not their
  fault → back to PENDING, the leg is reassigned.

**Changes.** A change only re-opens the signatures it affects (swapping leg 2 never asks the seller or leg 1).
Three kinds:
1. **Affects only me** → applied at once; the others are told.
2. **Affects others** → **proposed**; the affected parties approve; **silence falls back to a written default** by
   the deadline (so one silent party never blocks the deal).
3. **Rule-based** (NexGate / the system) → applied under the rule, logged.

| Change | Kind | Who must approve | No answer by the deadline |
|---|---|---|---|
| Buyer edits before paying (DRAFT) | 1 | — | — |
| Buyer cancels the whole order before the seller signs | 1 | — | — |
| Seller cancels (pickup before collected / delivery before Ready; pop-up "I informed the buyer", §3.1) | 1 | — (buyer notified at once) | — |
| Buyer cancels the shipping / switches to pickup (before pickup) | 1 | — (shipper released and told; fee if heading to the shop) | — |
| Buyer changes the destination **before any shipper accepted** | 1 | — (re-quoted at today's prices) | — |
| **Buyer changes the destination after a shipper accepted** | 2 | the shipper(s) whose leg changes | **original destination stays** |
| Accepted leg's time moves > 4 h | 2 | that shipper (re-confirm) | treated as "no" → leg reassigned |
| Reassignment — same / cheaper and not later | 3 | — | — |
| Reassignment — dearer / slower / nobody | 2 | the buyer | 24 h, then automatic switch to pickup |
| Chain not fully accepted 24 h after Ready | 2 | the buyer | 24 h, then automatic switch to pickup |
| Bulky mismatch | 3 | — (seller pays the difference automatically) | — |
| Admin action (§6.7.2) | 3 | — (reason required, audit-logged) | — |

**Destination change (buyer) — when it is allowed** (like AliExpress: the app shows **"Change delivery location"**
only in these statuses, and says up front when a courier must approve):

| Order status | Change? | How |
|---|---|---|
| **placed / accepted** (before Ready) | ✅ freely | no approval — re-quoted at today's prices; same shipper covers it → only the cost changes (pay / refund the difference); else the buyer picks a new option |
| **ready, offers out, nobody accepted** | ✅ freely | same; pending offers withdrawn, new ones sent |
| **shipper accepted** (incl. heading to the shop) | ✅ with approval | below — decline = that leg reassigned |
| **picked up / on the way** | ✅ with approval | below — decline = the original location stays |
| **chain, some legs done** | ✅ with approval | only the legs still to come are asked |
| **delivered / collected / completed** | ❌ | — |
| **pickup order** | — | "Deliver it instead" (§3.4) until collected |

**Limit: 3 location changes per order**; after that only support (admin) can change it.

**With approval.** The buyer proposes a new pin; NexGate checks the affected shipper covers it and
prices it from that shipper's table. The shipper sees old and new side by side (response window 30 min):

| Shipper answers | Result |
|---|---|
| **Accept at the new cost** | the buyer pays the difference into that leg's escrow, or gets it back if cheaper |
| **Accept at the old price** (waives the increase) | nothing more to pay |
| **Decline — before pickup** | that shipper withdraws → the leg is reassigned (§10a: automatic if same / cheaper and not later, else the buyer decides) |
| **Decline — after pickup** | the shipper **cannot withdraw** (never abandon a carried parcel) → the **original destination stays** |

- Only the legs whose from / to changes are asked (a new final door asks the last shipper only).
- New pin outside the shipper's coverage: before pickup → handled like a decline (reassignment); after pickup →
  not possible now (later: paid redelivery, §15b).
- Not allowed once the parcel is **delivered**.

**Sketch of the entity:**
```
shipping_agreement     order, status, version
 ├ agreement_party     role (buyer / seller / shipper), who, leg, signed / declined / withdrawn + when
 ├ agreement_leg       from / to place, shipper, cost, ETA        ← each LEG escrow part points here
 └ agreement_change    type, proposed by, who must approve, deadline, default, status, before / after
```

## 5. Group purchase and installments

**Group purchase**
- Each member chooses **their own** pickup / delivery and pays the delivery fee **with their payment**.
- Group fails or member withdraws → delivery fee refunded with the items. Transferred payment → fulfillment moves with it.
- **Price frozen at payment**, even if the group fills days later; a shipper rejecting then → normal reassignment.

**Installments**
- **Immediate** (ships after the down payment): the full delivery fee is paid **with the down payment**.
- **Layaway** (ships after the last instalment): **no shipping at checkout**; pickup / delivery is chosen and the
  fee paid **after the last instalment** (same mechanism as "switch to delivery").
- Agreement cancelled before shipping → delivery fee refunded.

---

## 6. Shippers

### 6.1 Two kinds
| | **Native** (delivery business on NexGate) | **Integrated** (external provider) |
|---|---|---|
| Set up by | owner, in the app | NexGate: one adapter + an admin row |
| Coverage / prices | owner's tables | the provider's API |
| Jobs | offer in the **app** (required); SMS = notifications only | NexGate calls their API |
| Status | owner taps in the app | their webhooks / polling, mapped to our statuses |
| Money | owner's NexGate wallet | a **provider ledger account**, settled by NexGate per contract / invoice |
| Delivery proof | our codes | their proof of delivery, else the buyer's "received" button |
| Chains | can be any leg | **single leg only** (whole trip, in one city) |

Both answer the same contract: **covers? · quotation (cost + ETA) · book · cancel · status** — the engine never changes.

**Shipper registry:** native shippers **register themselves** (§6.2). Integrated providers are **defined by the system**
(an adapter in code + one row created by NexGate) — never registered through the app; NexGate controls them fully
(on / off, API keys, categories, limits, spend cap). Each integrated provider has its **own ledger account** and an
**admin status view** (live rides, today's spend, balance owed, receipt differences, failed calls, webhook health).
The engine and the admin shipper list treat both kinds the same way.

### 6.2 Native registration — two parts
**Part 1 — KYC** (`POST /api/v1/shipping/businesses`) — legal, checked once, **auto-approved**:
business name · contact phone · **office location** (exactly one; pin screen like shops) ·
**business licence** (file + number + expiry; number unique) · **director's NIDA copy** (file + number) ·
**LATRA licence** (file + number + expiry). Documents are private (owner + admins).
→ status `KYC_APPROVED` (not live).

**Part 2 — Service Setup** (own endpoints, any time after KYC: `.../{id}/setup/profile|hours|coverage|pricing|limits`,
`GET .../{id}/setup` = checklist):
profile (logo, description, WhatsApp, email, vehicle badge) · service type (door and / or station) ·
working hours · coverage · pricing (station businesses: optional **collection from shops** — areas + fee, 0 = free) · limits (max value, accepts bulky + extra, response window, heads-up on/off).

**Live:** automatically once it has service type + coverage + pricing (hours default Mon–Sat 08:00–18:00).
Statuses: `KYC_APPROVED → LIVE ⇄ PAUSED` (owner), `SUSPENDED` (admin / automatic). A user may own several.
Needs two new File Thunder upload types: private business document, business logo.

### 6.3 Working hours (shared piece: businesses, shops, stations)
Several ranges per day · special days (holidays) · temporarily closed · same-day cut-off · EAT · overnight ranges allowed.

### 6.4 Shops: hours and pause
- Shops get opening hours (same piece): "Open now / Opens Sat 09:00"; seller timers count only open hours.
- **Pause** (existing `TEMPORARILY_OFFLINE` + until + reason) gets a mode chosen by the seller:
  **Keep selling — ships when back** (timers wait) or **Stop orders** (Buy off, `SHOP_CLOSED`).
  Orders placed before the pause must still be handled. Reopens automatically at "until".

---

### 6.5 Integrated provider: Bolt (first and only one for now)

API reference in the repo: `docs/shipping/providers/bolt/Bolt-Ride-Booker-API-Docs.pdf` (Ride Booker API).
**Bolt confirmed the same API is used for parcels.** The **Bolt Business account is ready.**

**NexGate is a reseller of Bolt.** The API is literally `/reseller/v1/...`: NexGate books rides with its business
account, **owns the customer** (buyer and seller never deal with Bolt), Bolt **bills NexGate monthly**, and claims /
refunds go NexGate → Bolt support (email) → credited on the monthly statement. To the buyer, Bolt is just one
delivery option sold by NexGate.

**Where Bolt is used**
- **Single leg, in one city, door-to-door only.** Never inside a multi-shipper chain (no handover codes, its price
  `fare_id` lives 5 minutes, a ride can go back to "searching").
- Bolt offers **boda** in Tanzania → **small, non-bulky parcels only**.
- Usually ranks ⚡ Fastest; ranked like any other option.

**Mapping to the contract**
| Our question | Bolt |
|---|---|
| Covers? | `estimations` — boda category with an ETA; `eta: null` or `AREA_NOT_SERVICED` = no |
| Cost + ETA | `estimations` → `price {minimum_amount, maximum_amount}`, `eta` |
| Book | at **Ready**: estimate again → check the held price → `POST rides` (`fare_id`, valid 5 min, single use) |
| Cancel | allowed until the ride has started |
| Status | webhooks (live only, from Bolt's 3 fixed IPs, with our token) or polling ride details (≤ every 10 s) |

| Bolt state | Our meaning |
|---|---|
| SEARCHING | offer out |
| DRIVER_ON_ROUTE_TO_CLIENT | accepted — rider coming to the shop |
| ARRIVED_AT_CLIENT | rider at the shop |
| DRIVING_WITH_CLIENT | picked up → on the way |
| COMPLETED | delivered (Bolt's proof) → buyer confirms with the button |
| NO_DRIVER_FOUND / CANCELLED | rejected → reassignment (another Bolt try or a native shipper) |
| back to SEARCHING after accepting, > ~10 min | cancel the Bolt ride → reassignment |
| CLIENT_DID_NOT_SHOW (shop didn't hand over) | **seller pays** Bolt's charge (from the items part at release); re-book on the next Ready |

**Price**
- Quotation shows Bolt's **maximum amount + 2%**. NexGate absorbs any difference within that (Bolt's final price
  can move — the receipt may change up to 24 h later).
- At Ready: new estimate within the held price → book; above it → treated as a dearer reassignment (buyer decides).
- Real charges recorded on the **Bolt ledger account**, reconciled monthly with Bolt's invoice.

**On the ride**
- Bolt "user" = **the shop's contact** (driver calls them for pickup); the **receiver** goes in the note to the driver
  (template: "NexGate parcel. Pickup: <shop, landmark>. Deliver to: <receiver name, phone, directions>. Do not open.").
- Bolt returns the driver's **name, photo, phone, plate** and a **tracking link** → shown to seller (check the plate at
  pickup, instead of a code) and buyer ("with Juma, boda T 123 ABC — track").
- Codes: Bolt drivers can't enter ours → pickup = plate check; delivery = Bolt COMPLETED + buyer's "received" button.

**Control**
- Admin **kill-switch** (Bolt off instantly) and a **daily spend limit**.
- OAuth2 client credentials, tokens 10 min — backend only, cached and refreshed. Sandbox available for building.

### 6.6 The shipper app (native shippers)

**Same NexGate app**, in a "delivery business" mode (like managing a shop) — no separate rider app.
**For now only the owner** uses it; team members (dispatcher / rider roles, assigning jobs, the rider's name shown
to buyers) come later when businesses grow. Earnings always go to the owner's wallet.

| Screen | What it's for |
|---|---|
| **Dashboard** | today's jobs, open offers, **upcoming orders**, earnings held / released, reliability, "Paused" banner |
| **Upcoming** | orders in a **paid agreement** with this shipper, **from payment**, before any offer (below) |
| **Offers** | job offers with a **countdown**: from / to on a map, parcel value band, bulky, cost, when the parcel reaches them (chains) → **Accept** / **Reject (reason)** |
| **Heads-ups** | "order coming from Sinza in ~15 min" (non-binding) |
| **Active jobs** | exact from / to (office, map, phone, hours), receiver, note, navigation, **one button per next step** |
| **Codes and handovers** | show the pickup code to the seller; enter the buyer's delivery code; chain "handed over" / "received" + code; optional photo |
| **Re-confirms** | "parcel now reaches you at 23:00 instead of 15:00 — still OK?" |
| **History · Earnings · Reviews & reliability · Setup** | past timelines · held → released per job, wallet · ratings, replies, scores · KYC, Service Setup, pause / resume |

**Upcoming orders**
```
Upcoming (5)
🕐 Kariakoo → Mikocheni · small · 7,000        Waiting for the seller to accept
📦 Sinza → Mbezi · bulky · 22,000              Seller is packing — offer expected soon
🚌 Leg 2: Magufuli → Mbeya Stand · 21,000      Parcel expected at your office tomorrow ~10:00
```
- **Shippers see a parcel, never the order**: parcel number, size / bulky, value band, from / to, their cost —
  not the items, prices or order number.
- Shown **from payment**; chips: waiting for the seller → seller packing (= heads-up) → for chain legs, when the
  parcel is expected. On **Ready** the card moves to **Offers**.
- **Not a commitment** (accepting is at the offer). Cards can disappear (buyer cancels, switches to pickup, seller
  times out) with a short note.
- **Privacy:** until the shipper **accepts the offer** it sees **areas only** (ward / district), parcel size / bulky,
  value band and its cost — **not** the exact pin, directions or receiver phone. Full details after accepting.

**Endpoints (sketch)** — also pushed live to the app, like chat:
```
GET  /api/v1/shipping/businesses/{id}/upcoming
GET  /api/v1/shipping/businesses/{id}/offers
POST /api/v1/shipping/offers/{offerId}/accept | /reject      (reason)
GET  /api/v1/shipping/businesses/{id}/jobs?status=active
POST /api/v1/shipping/jobs/{jobId}/status                    (next step)
POST /api/v1/shipping/jobs/{jobId}/handover                  (code + optional photo)
POST /api/v1/shipping/jobs/{jobId}/reconfirm
GET  /api/v1/shipping/businesses/{id}/earnings
```

### 6.7 Admin side and settings

**Build order:** the admin **dashboard (screens) is the last thing built**, after everything works. But the admin
**endpoints** needed to fix stuck orders ship **with** the shipping service (no screens yet) — §6.7.2.

#### 6.7.1 Where each setting lives

**1 — Properties file: fixed platform rules** (change = config + redeploy)
| Setting | Value |
|---|---|
| Meeting-point range | 1 km |
| Loop guard (reassignments per order) | 3 |
| Re-confirm threshold for accepted legs | 4 h |
| Chain safety net / buyer decision timeout | 24 h / 24 h |
| Closed shipper: wait if it opens within | 4 h |
| Seller accept window | 24 opening hours |
| Pickup collection window (shop) / station + shipper-office hold | 14 days / 7 days |
| Destination changes per order | 3 |
| Quotation price hold | 5 days |
| Outlier rule / "expensive parcel" | 3× / 500,000 |
| Quotation options shown | top 5 |
| Codes: length / max wrong tries | 4 digits / 5 |
| Distance factor / rounding | ×1.3 / up to the nearest 100 |
| Estimation cache | 1 h |
| Auto-pause: missed offers warn / pause | 2 / 3 |
| Auto-pause: suspend after auto-pauses | 3 within 7 days |
| Acceptance rate warn / pause (last 20) | 50% / 30% |
| Broke binding: suspend after / found at fault: suspend after | 2 in 30 days / 2 in 30 days |
| False bulky claims → suspend | 3 |
| On-time warning (last 20 jobs) | 60% |
| Grace period (rate rules) | first 10 offers |
| Bulky mismatch: wasted-trip fee / auto-flag after | 50% of the leg cost / 2 mismatches (different shippers) |
| Defaults shippers can override | response window 30 min · station reach 30 km |
| Bolt | price buffer 2% · category boda |

**2 — Per shipper, set by NexGate** (the NexGate ↔ shipper agreement)
| Setting | Notes |
|---|---|
| **Delivery commission %** | default = a new fee type in the effective-dated fee table (0% now); a shipper with its own deal gets an **override with a start date** (past orders keep the old rate) |
| Settlement terms (integrated) | Bolt: monthly invoice |
| Daily spend limit (integrated) | e.g. Bolt 2,000,000 / day |
| On / off (integrated) | the kill-switch |
| Suspended / lifted (native) | with a reason the shipper sees |

**3 — Per shipper, set by the shipper** (Service Setup): hours, coverage, stations, prices, limits, heads-up on/off,
its own response window, its own station reach.

**4 — Admin-managed data**: free-shipping campaigns, integrated provider records, review moderation, manual money actions.

#### 6.7.2 Admin endpoints that ship WITH the shipping service (no screens)
`SUPER_ADMIN` only · every call needs a **reason** · every call is written to the **audit log** (who, what, when, why).

| Endpoint (sketch) | Why it's needed from day one |
|---|---|
| `POST /api/v1/admin/shipping/escrows/{escrowId}/release` · `/refund` | stand-in for disputes: fix a stuck or reported order by hand |
| `POST /api/v1/admin/shipping/orders/{orderId}/reassign` · `/cancel` | unstick a chain |
| `POST /api/v1/admin/shipping/businesses/{id}/suspend` · `/lift` | stop a bad shipper |
| `POST /api/v1/admin/shipping/providers/{code}/enable` · `/disable` | Bolt kill-switch |
| `PUT  /api/v1/admin/shipping/businesses/{id}/commission` | per-shipper commission override (with start date) |

#### 6.7.3 The dashboard (built last)
| Area | Contents |
|---|---|
| **Native shippers** | list / search / filter; one shipper's page: profile, **private KYC documents**, coverage map, stations, prices, reliability, reviews, jobs, earnings; suspend / lift; hide review; documents expiring soon |
| **Integrated providers** | kill-switch, spend limit, category, buffer; **status view**: live rides, today's spend vs limit, balance owed, receipt differences, failed calls, webhook health; **settlement**: record the monthly invoice, match the ledger, mark paid. API keys live in **Vault**, never in the database |
| **Operations board** | stuck things: chains not fully accepted, buyer decisions waiting, money held a long time, problem reports, Bolt rides stuck searching — with the §6.7.2 actions and internal notes |
| **Campaigns** | create / edit / pause / stop; budget used / reserved / left |
| **Reports** | by ward / district / region only (never per person): deliveries, on-time rate, average cost, pickup vs delivery, shipper ranking, Bolt spend |
| **Audit log** | every admin action |

**Who can do what (when the dashboard comes):** `STAFF_ADMIN` — view everything, suspend / lift, hide reviews, notes,
force reassignment. `SUPER_ADMIN` only — anything with money (release / refund, settlement), settings, Bolt
kill-switch and limits, campaigns, commission overrides.

## 7. Coverage

### 7.1 One model for door and station businesses
1. **Where do you work?** Tick areas from the region → district → ward list (with search). A read-only map
   shows the ticked areas to confirm. No radius, no drawing.
2. **Trips:** `WITHIN_EACH` (both ends inside the same ticked area) or `WITHIN_AND_ACROSS` (both ends inside any
   ticked areas — crossing borders between them). **Both ends must always be inside the ticked areas.**
3. **Lines** (optional, station businesses with separate lines): groups of areas; trips only between areas of the same line.

### 7.2 Stations (station businesses)
- **As many stations as needed**; at least **one per ticked area**, several per area allowed (e.g. Dar: Magufuli,
  Kariakoo, Ubungo). A station = a pin (Google map + our resolve, like a shop) + counter name + phone + own hours.
  Each business has its own stations.
- **Reach:** the buyer collects within **30 km** of a station (the shop side is reached by a door leg, §7.2a).

### 7.2a Handover points — where each leg picks up and drops off
1. **Leg 1 always starts with a collection at the shop** (**option A, decided 2026-10-13**) — the seller never
   leaves the shop. Who collects:
   - **A station business that collects from shops** — it ticks the areas where it collects (same area picker) and
     sets a **collection fee** (flat; **0 = free**). How it gets the parcel to its station is **its own business**;
     we don't model it. The collection is **part of its leg**: one leg, one business, one escrow part, the pickup
     code at the shop, "heading to the shop" applies.
     ```
     Leg 1 🚌 Shabiby (collects free)  shop (Ubungo) → Mbeya Bus Stand   21,000   1 day
     ```
   - **Otherwise a door shipper** — the station route becomes a **chain**: door leg shop → station, then the
     station leg(s):
     ```
     Leg 1 🚪 boda     shop (Ubungo) → Magufuli Terminal        3,500
     Leg 2 🚌 Shabiby  Magufuli Terminal → Mbeya Bus Stand     21,000   → buyer collects, or leg 3 door
     ```
   - Neither → no station route from that shop (door-only options, else pickup only).

   The engine builds both kinds side by side; **no special boost** — free collection wins on cost by itself, and
   **"fewer handovers"** breaks ties when cost and arrival are equal. After launch (§16): the seller may drop at stations.
2. **Station businesses hand over only at their own stations** (a bus company never drives to a door).
3. **Door businesses go wherever the leg needs**, inside their coverage: the shop, a station, the buyer's door.
   **Door shippers always come to the shop** for leg 1 (the seller does not drop at a door shipper's office — after launch).
4. The engine chooses **which** station: origin = a station a door shipper can reach from the shop (both inside that
   door shipper's coverage), nearest first; destination
   = its station nearest the buyer (within reach) **and** priced in its pair table; bus → bus offices within **1 km**.
   Several stations in reach → each is a candidate; ranking picks; the others are alternatives in the quotation.
5. **Every leg in the agreement stores its exact from / to place** (office name, pin, phone, hours), which is what
   that shipper's app shows ("Collect at: Shabiby office, Magufuli Terminal, gate 3 · [map] · open 06:00–20:00").

### 7.3 How the engine checks a trip
1. **Fast filter by area codes** (no maps): both ends inside, trips rule, line rule.
2. **Stations** (station businesses): origin station reached by a door leg from the shop (§7.2a), destination station
   nearest the buyer within reach.

### 7.4 Coverage request (sketch)
```json
PUT /api/v1/shipping/businesses/{id}/setup/coverage
{ "areas": [ { "level": "REGION", "regionCode": "07" },
             { "level": "DISTRICT", "regionCode": "05", "districtCode": "03" },
             { "level": "WARD", "wardId": 1146 } ],
  "trips": "WITHIN_AND_ACROSS",
  "lines": [ { "name": "Line A", "areas": [0, 1, 2] } ],
  "stationReachKm": { "seller": 30, "buyer": 30 } }
```
Location helpers: region → district → ward lists + search; coverage outline for the map.

---

## 8. Pricing and ETA

### 8.1 Cost of one parcel
```
base (shipper's table) + value add-on (insurance) + bulky extra = cost
```

**Base**
- **Door — distance bands** (price + time each). Distance = straight line × 1.3. Last band's end = max distance.
  ```
  0–3 km   2,000   30–60 min
  3–7 km   3,500   1–2 h
  7–15 km  5,000   2–3 h
  ```
- **Door — flat**: one price + one time.
- **Station — pair table**: from station → to station, price + time, "both ways" ✓. The app pre-fills the pairs;
  an empty pair = not carried.
- **Station — collection from shops** (optional): areas where it collects + one flat **collection fee** (0 = free) +
  the time it adds. Cost = collection fee + the pair price from its station nearest the shop.

**Value add-on** (optional ladder; fixed amounts per value range, % allowed for the top range) + **max value accepted**.
Parcel value = the shop's items in the cart.

**Bulky**: new product field `bulky` (category can pre-tick); a parcel is bulky if any item is.
Shipper: accepts bulky yes / no + extra. Non-accepting shippers are filtered out.

**Rounding**: whole TZS; add-ons rounded **up** to the nearest 100. New prices apply to new quotes only.

### 8.2 ETA
- Always **from when the shop is ready**, always a **range**.
- Door: the band's time. Station: the pair's time. Plus closed hours (shipper / station hours, cut-off).
- Record real delivery times from day one (reliability-based ETA after launch).

### 8.3 Fees
- **Delivery commission**: new fee type, **0%** for now, deducted from the shipper's part.
- **Seller's platform fee: items + tax only** (changes today's "fee on full total incl. shipping").

---

### 8.4 Bulky mismatch (product not ticked "bulky" but it is)
Rule: **the seller pays** (they set the flag), **the buyer's price never changes**, repeated mistakes **fix the flag**.

1. **Shipper reports at the shop, before pickup**: "Can't carry — too big / heavy" (+ optional photo). Not possible
   after the pickup code is used.
2. **That leg is reassigned for bulky only**: rejection reason `BULKY_MISMATCH` — **not counted against the shipper's
   reliability**; the parcel is now bulky → only bulky-accepting shippers, with their bulky extra.
3. **Money**: the replacement's extra cost is paid **by the seller automatically** (taken from the items escrow at
   release). The buyer is **not asked** — just told "too big for a boda; a car is coming, no extra cost to you".
4. **Wasted trip**: native first shipper gets **50% of its leg cost** (properties setting), paid by the seller;
   Bolt → Bolt's cancellation charge, paid by the seller.
5. **Nobody can carry it**: the buyer chooses **pickup** (delivery refunded) or **cancel the whole order** (full
   refund — seller's fault); the seller still pays the wasted trip.
6. **Fixing the flag**: 1st mismatch → seller told to tick "bulky" (with the photo); **2nd mismatch on the same
   product from a different shipper → bulky set automatically**; mismatches count in the seller's private reliability.
7. **Abuse**: a shipper claiming mismatches on products others carry fine → flagged for admins.
8. **Opposite case** (ticked bulky, actually small): no automatic change — higher prices are incentive enough.

### 8.5 Packing — the seller packs
- **The seller packs** (like AliExpress / Jumia): they know the item (fragile, leaking, box vs bag); packing happens
  before any shipper has the parcel; the box is part of the seller's item price, never a separate charge. The Ready
  pop-up ("I have packed it well", §10) records it.
- **Liability**: damaged because of poor packing → the seller; packed well and crushed on the way → the shipper
  (decided from the handover timeline and optional photos).
- **The shipper checks the outside at pickup** (QR scan, a look, optional photo).
- **"Badly packed" refusal** — same pattern as a bulky mismatch (§8.4): **at the shop, before pickup** only; reason
  `BADLY_PACKED` (torn, leaking, a fragile item with no protection) + photo; **not counted against the shipper**.
  The seller **repacks and taps Ready again** → the same shipper is offered it again (normal window), else the leg
  is reassigned. The seller pays the **wasted trip** (native 50% of the leg / Bolt's charge), from the items escrow
  at release. The buyer's price never changes; the buyer is told "the seller is repacking your parcel".
- Repeated `BADLY_PACKED` reports count in the seller's private reliability; a shipper reporting it on parcels
  others carry fine → flagged for admins (as §8.4 rule 7).

## 9. Engine and ranking

1. **Where**: both places' ward / district / region.
2. **Who**: native coverage + integrated providers (skip a provider slower than ~2 s).
3. **Filter**: coverage, max distance, max value, bulky, closed / cut-off (ETA shifts, not dropped), paused / suspended.
4. **Cost + ETA** for each.
5. **Clean-up**: hide **dominated** options (dearer *and* slower) and **outliers** (> 3× the cheapest of the same type) — "See all" shows them.
6. **Labels**
   - 💰 **Cheapest** — ties → earlier arrival → reliability.
   - ⚡ **Fastest** — by the **latest** arrival of the range — ties → cheaper.
   - ⭐ **Recommended** — balance score; parcel > 500,000 → reliability-heavy; cheap parcel → price-heavy; else
     price + speed. **At launch: price + time only** (reliability added after launch).
   - One option may carry two labels.
7. **Order**: labelled first (⭐ 💰 ⚡), then grouped "To your door" / "Collect at a station" by cost; top 5 + "See all".
8. **Fairness**: new shippers get a neutral reliability + "New" badge; exact ties rotate.
9. Quotations hold their prices **5 days** (§4.1); the sheet pre-selects ⭐.

---

## 10. Job flow

1. **Seller accepts** within **24 opening hours** — else auto-cancel + full refund. Shipper gets a non-binding
   **heads-up** (switchable per shipper).
2. **Seller taps Packed / Ready** → a **confirm pop-up** with two ticks, both required:
   ☐ *"I have packed it well"* · ☐ *"The parcel label (QR) / number is on the package"* (delivery only; pickup asks
   only the first). Recorded with the time — part of the evidence trail if a parcel arrives damaged or unlabelled.
   Then → pickup: buyer told; delivery: the offer goes to **every leg at once**, each
   showing when the parcel reaches them. **The parcel does not move until all legs have accepted.**
3. **Response window 30 min** (per shipper), counted only while the shipper is open. Shipper closed at Ready →
   wait if it opens within ~4 h, else reassign.
4. **Accept** = the shipper **signs the agreement** (§4.8) — capacity, not price (the price is the agreement). **After accepting it is binding: the shipper must
   find a way** — no buyer rescue mid-way; failing = heavy reliability hit, auto-pause, dispute / liability.
   **Reject (reason) or timeout before accepting** → reassignment (§10a).
5. **Who taps what**: seller — accept, ready, verifies the pickup code; shipper — accept / reject, every status,
   submits the delivery code; buyer — gives the code (or the button), confirms "it's what I ordered".

### 10a. Reassignment (a shipper rejects or doesn't answer — before accepting)

Only **before** a shipper accepts; after accepting it is binding.

1. **Record**: leg marked *rejected* (with reason) or *timed out* — a timeout counts exactly like a rejection.
   That shipper is excluded from this order and takes a reliability hit; **3 missed offers in a row → auto-pause**.
2. **Replace that leg only.** Other legs keep their acceptance; **nothing moves until every leg has accepted.**
   The replacement is the best-ranked option using **the label the buyer chose** (💰 → cheapest replacement …).
3. **Search in steps — stop at district:**

   | Step | Replacement | Neighbouring legs |
   |---|---|---|
   | **1. Nearby** | office within **1 km** of the original handover points | keep shipper + cost; get the updated exact office; **no re-confirm** |
   | **2. Same district** | office in the **same district** as the original point | door legs **re-route** inside their coverage and are **re-costed**; the **total of the affected legs** is compared; neighbours already accepted get a **re-confirm** ("drop at Ubungo Terminal instead, OK?") |
   | **3. Beyond the district** | — no automatic swap | **buyer decides** |

   Door legs follow the same steps (first the same points, then a meeting point elsewhere in the district).
4. **Compare with the agreement** (for the affected legs):
   - **same or cheaper AND arriving no later** (by latest arrival time) → **automatic**; the buyer is told; leg escrow **re-pointed**,
     difference **refunded**;
   - **dearer / slower / nobody** → the **buyer decides** while the parcel is still at the shop: pay the difference
     (top-up into the same leg escrow) · end the chain earlier (later legs refunded) · switch to pickup (all legs
     refunded) · cancel (everything refunded).
5. The new shipper gets the offer (normal response window) — and may reject too (back to step 1).
6. **Loop guard: 3 reassignments per order** (not per leg) → after that the buyer is always asked.
7. **Closed shipper at Ready**: its window starts when it opens if that is within **4 h**; otherwise reassign at once.
7a. **Accepted legs whose time moves**: moved by **≤ 4 h** → an update, still bound; moved by **> 4 h** → **re-confirm**
    ("now 23:00 instead of 15:00, still OK?"); "no" → that leg is reassigned (counts toward the 3 per order).
7b. **Chain safety net**: not fully accepted **24 h after Ready** → the buyer is asked: keep waiting · end the chain
    earlier · pickup · new delivery proposal · cancel the whole order.
7c. **Buyer doesn't answer a decision screen**: reminders, then after **24 h** → **automatic switch to pickup**
    (leg escrows refunded; the buyer can still ask for a new delivery proposal).
8. **Free shipping**: seller / campaign caps are recalculated for the new cost; the buyer pays only what's above them.
9. **The agreement keeps its history** ("leg 3: Mbeya Riders rejected 10:42, busy → Mbeya Express 2,500").
10. **Who is told**: buyer (notice on auto swaps / decision screen) · seller ("waiting for a courier", nothing to do) ·
    legs already accepted ("still waiting for the full route", or a re-confirm when their point moved).

---

## 11. Codes and handovers

**Parcel number** — identifies the parcel (the codes below prove the handover):
- One per parcel (one shop order), created when the **seller accepts** the order — so the label can be printed while
  packing, before the Ready pop-up asks for it. Every order gets one, **pickup too** (the counter finds the parcel on
  the shelf by it; a later switch to delivery keeps the same number); short and readable aloud, no 0/O or 1/I — e.g. **`NX-4K7P-2Q`**.
- **The same on every leg**: boda, bus company and station staff all see it; station staff **search by it** on
  arrival and when the buyer collects; the buyer sees it too.
- **QR label at launch, made by the app (frontend)** — the backend only gives the parcel number. From **accept** the
  seller's app shows a label: **QR (the parcel number only — no personal data)** + the number in large
  text + receiver name + from / to areas → print or share it to a printer. **No printer → the seller writes the
  number on the box** (the fallback always works).
- **Checked at every pickup / handover**: the receiving shipper (or station staff) **scans the QR** or types the
  number → the app confirms
  **"✓ this parcel is yours"** or **"✗ wrong parcel — not on your job"** before the code is used. Stops the wrong box
  leaving the shop or a busy station.
- Not secret (it's on the box) and separate from the order number, which never reaches shippers.

| | Parcel number `NX-4K7P-2Q` | 4-digit codes |
|---|---|---|
| Purpose | **identifies** the parcel | **proves** a handover |
| Secret? | no — written on the box | **yes** — hashed, never logged |
| How many | one per parcel, for life | one per handover |

**Codes**
- **4 random digits**, hashed, never logged, valid for one order. **5 wrong tries** → locked; the owner gets a new one.
- **Pickup code** — shipper receives it after accepting; at the shop the **seller verifies** it, then hands over
  (proves the right shipper collected).
- **Delivery code** — buyer receives it once the parcel is on the way; the **shipper submits** it at the door.
- **Shipper → shipper** (multi-shipper): the giver taps "I've handed over", the receiver taps "I've received" and
  enters that handover's code; optional photo from either side. One side missing → flagged for disputes.
- **Pickup at the shop / collection at a station** — the same delivery code, entered by the seller / station staff.
- **Buyer option (account setting):** use a **"received" button** instead of the code. Default = code.
- **Handover photos**: optional for now (stored if taken) — the evidence trail for disputes.
- Integrated providers: their proof of delivery, else the buyer's button.

---

## 12. Money

### 12.1 Escrow parts per order
```
Buyer pays once → ORDER ESCROW
   ├ items part  → seller (fee: items + tax)
   └ leg part    → shipper owner (native) / provider ledger account (integrated)   ← one per leg
   (pickup = items part only)
```
"Held" = in escrow, nobody's yet. **Release** = to the payees. **Refund** = back to the buyer.
Each part can be released or refunded on its own.

**One escrow record per part, all children of the order:**
```
Order ORD-…  (one shop's parcel)                                     ← parent
 ├ ESC-…101  part ITEMS  payee seller                    60,000  HELD
 ├ ESC-…102  part LEG    payee Kibuti Express   legId L1  3,500  HELD
 ├ ESC-…103  part LEG    payee Shabiby          legId L2 21,000  HELD
 └ ESC-…104  part LEG    payee Mbeya Riders     legId L3  2,500  HELD
```
- Each record: `orderId` (the escrow table's existing, unused field — now filled) · `part` (ITEMS / LEG) ·
  `legId` · its own payee (user or provider account) · amount · fee · status · ledger account.
- Why separate records: each part moves on its own (refund only leg 3, re-point only leg 2, pay an innocent
  rider in a dispute); everyone's history shows only their own money; the ledger already works per escrow;
  it is the cart-fix mechanism (one payment → several escrows), whose `session_part` key folds into this.
- **Release** ("it's what I ordered") = every escrow of the order, found by `orderId`, in **one transaction**.
- The buyer still sees **one payment** line listing its escrows.
- Table changes: `part`, `legId`, `orderId` filled, and a **provider payee**.

### 12.2 When money moves
- **Everything stays held until the buyer taps "✓ It's what I ordered"** (after receiving) — then **all parts are
  released together** (seller and shipper).
- **"⚠ Report a problem"** → **all parts frozen**; the dispute involves buyer, seller **and the shipper**
  (the rider could have swapped or damaged the parcel); the admin decides from the handover timeline.
- **No automatic release for now.**

### 12.3 Adjustments and refunds
| Event | Money |
|---|---|
| Reassign — same / cheaper | leg part re-pointed to the new shipper; difference refunded |
| Reassign — dearer | buyer tops up the difference into the same leg part, or switches / cancels |
| Pickup → delivery | new payment for the leg part(s) |
| Delivery → pickup / cancel the shipping, shipper not heading to the shop yet | leg part(s) refunded |
| Cancel the shipping, shipper heading to the shop | leg part refunded minus the wasted-trip fee (native 50% of the leg / Bolt's charge), which goes to the shipper |
| Destination change | difference paid into / refunded from that leg part (nothing if the shipper keeps the old price) |
| Cancel before the seller accepts / pickup not collected in 14 days / seller cancels (§3.1) / group fails | all parts refunded |
| Installment immediate / layaway | legs with the down payment / a new payment at the end |

The escrow table must allow a **provider payee** (today every escrow needs a user).

---

## 13. Free shipping

**Seller-funded** (shop setting): areas (same picker) · optional min order (this shop's items) · "I pay up to X per
delivery" · product override (use shop / always free / never free). Mixed parcel: free only if every item qualifies.

**NexGate campaigns** (admin): areas · dates · min order · cap per delivery · audience (everyone / first order) ·
shops (all / selected) · total budget → stops automatically; budget **reserved at payment**.

**Rules**
- The **cheapest qualifying option** becomes free (up to the cap); faster options cost the difference.
- **Stacking**: delivery cost − seller (cap) − NexGate (cap) = buyer pays.
- **Shipper always paid in full**: leg part holds the full cost = buyer's share + NexGate's share (from the
  campaign ledger account at payment) + seller's share (taken from the seller's items part at release).
- Refund → NexGate's share returns to the campaign budget.
- Display: ~~3,500~~ **Free**; card badge "Free delivery"; search filter "Free delivery to my area".

---

## 14. Reviews, reliability, auto-pause

- **Reviews** like shops (rating 1–5 + text, owner reply, moderation statuses, summary) — **no subscribe** — and
  **only from people the shipper served** (buyers it delivered to, sellers it collected from), tagged
  "Verified delivery". Shown on the shipper profile and in quotes ("⭐ 4.6 (120)").
- **Reliability** (internal, automatic): acceptance rate, on time vs quoted ETA, failed / returned, disputes lost.
- **Auto-pause** — three levels, warnings first:

  | Level | Undone by | Effect |
  |---|---|---|
  | ⚠ Warning | — | notice ("one more missed offer and you'll be paused") |
  | ⏸ Paused | **owner** taps Resume | no new quotations / offers |
  | ⛔ Suspended | **admin only** (reason) | same + hidden from buyers |

  Accepted jobs are **always finished** (binding); agreements not yet offered are reassigned at Ready.

  | Signal | ⚠ Warning | ⏸ Paused | ⛔ Suspended |
  |---|---|---|---|
  | Missed offers in a row (timed out) | 2 | **3** | — |
  | Auto-pauses | — | — | **3rd within 7 days** |
  | Acceptance rate (last 20 offers) | < 50% | < 30% | — |
  | Broke a binding job (accepted, then failed) | — | 1st time | **2nd within 30 days** |
  | Found at fault in a problem report | 1st time | — | **2 within 30 days** |
  | False bulky-mismatch claims (flagged) | — | — | **3** (admin review) |
  | On time vs quoted ETA (last 20 jobs) | < 60% | — | — |

  - Rejecting **with a reason** is allowed — it only counts through the acceptance rate, never "in a row".
  - **Grace period**: rate rules (acceptance, on-time) apply only after the **first 10 offers**.
  - "In a row" resets on the next accepted offer; 7 / 30 days are rolling windows.
  - Every action shows the shipper **why** ("Paused: 3 offers missed — 10:02, 10:41, 11:15") and is recorded for admins.
  - Numbers live in the properties file. Integrated providers are not auto-paused (kill-switch instead).
- **Two-way ratings (private)**: shipper rates the buyer (address, no-show) and the seller (late handover).

---

## 15. Open — to talk about next

| # | Topic | Where we are |
|---|---|---|
| 1 | ~~Buyer cancellation rule~~ | **decided** — see §3.4a |
| 2 | **Buyer never confirms** (all parts held) | proposed: a check window after the delivery code; no decision |
| 3 | **Disputes** | **skipped for now** (decided). Draft discussed: buyer / shipper / seller can open; reasons; evidence + automatic handover timeline; 48 h to respond; outcomes no fault / full / partial / return & refund / replacement; shipper liability capped at its leg part; one module for digital + physical |
| 4 | **Failed delivery** (buyer unreachable, refuses the parcel, return trip) | with disputes |
| 5 | ~~Auto-pause numbers~~ | **decided** — §14 |
| 8–12 | ~~Reassignment proposals~~ | **decided** — §10a items 4, 7a–7c; door shippers always come to the shop (§7.2a) |
| 6 | ~~Cart: unsaved new pin~~ | **decided** — §4.4 |
| 7 | ~~Cart: default for a shop newly added~~ | **decided** — §4.4 |
| 13 | **Seller cancels a delivery order AFTER Ready** | open — before Ready is decided (§3.1); after Ready shippers have offers or signed (heading to the shop → who pays the wasted trip?) |

## 15a. Future — decide after implementation

Parked on purpose (2026-10-12): build the main flows first, then decide these with real cases.

| # | Scenario | What we know so far |
|---|---|---|
| F1 | **Buyer never confirms the delivery** (never there to receive / never gives the code) | idea: the shipper **holds the parcel** and it is **returned to the shipper's office**; how long, who pays storage / the trip back, what happens to the money — to decide |
| F1b | **Buyer received (code entered) but never taps "✓ It's what I ordered"** | all escrows stay held (no auto-release decided); a check window was proposed — to decide |
| F2 | **Failed delivery** — buyer unreachable, refuses the parcel, wrong address | "we will find a way later": re-attempt? return to the shop or the shipper's office? who pays? |
| F3 | **Parcel never collected at a station** | no rule yet (shop pickup has the 7-day rule); days to collect, storage, return — to decide |
| F4 | Disputes, returns, shipper liability | skipped for now — draft in §15 row 3 |

## 15b. Plan for the unhappy paths (recommended — NOT implemented now)

Based on common practice (Jumia, DHL-style couriers, Glovo, bus parcel services). **Principle: whoever caused
the failure pays for it.** These are recommendations to confirm after the main flows are built.

### Recommended rules

**F1 — Buyer not there to receive**
1. Rider **calls** the buyer and **waits ~10 min**, then marks **"attempt failed"** (time, location, optional photo).
   Buyer is notified: "We missed you — reschedule?"
2. **Up to 2 attempts**; the buyer picks a new time for attempt 2 in the app.
3. After the last failed attempt the parcel goes to the **shipper's office** and is **held 7 days** — the buyer can
   collect it there or ask for **redelivery** (pays a new delivery cost).
4. Not collected in 7 days → **returned to the seller**. Buyer refunded **items only**; the delivery cost is kept by
   the shipper (work done) and the return trip is charged to the buyer.

**F1b — Delivered (code entered) but the buyer never taps "✓ It's what I ordered"**
- **Auto-confirm after 3 days** (like digital products today); **7 days for parcels above 1,000,000**.
- Reminders at 24 h and 48 h. **"⚠ Report a problem" stops the clock.**
- Safe because the delivery code proves the buyer has the parcel.

**F2 — Failed delivery**
| Cause | Rule |
|---|---|
| Unreachable | F1 |
| Refused at the door, no reason | back to the seller; buyer refunded items **minus delivery + return** |
| Refused because wrong / damaged (seen at the door) | rider records reason + photo; back to the seller; buyer **full refund**; cost on the at-fault party (dispute) |
| Wrong / unclear address | rider calls; corrected spot **inside the shipper's coverage** → deliver (buyer pays any difference); else F1 |

**F3 — Parcel never collected at a station**
- Arrival notice; reminders on days 1, 3, 6.
- **7 days free** (shop pickup is 14 days — a shipper's shelf space costs them; a shop already holds the item).
- Not collected → **returned to the seller** via the reverse route; buyer refunded items **minus delivery + return**.
- Daily storage fee: possible later, not at first.

**Who pays**
| Failure caused by | Pays |
|---|---|
| Buyer (absent, refused without reason, never collected) | loses the delivery cost + pays the return; items refunded |
| Seller (wrong / damaged item seen at the door) | both ways; buyer fully refunded |
| Shipper (lost, broken, gave up after accepting) | its leg + liability (with disputes, later) |

### Plan — prepare now, switch on later

| Phase | What | When |
|---|---|---|
| **0 — data hooks (build WITH the shipping service)** | so the rules above slot in without rework: **delivery attempt records** (time, location, photo, outcome); **failure reason codes** (UNREACHABLE, REFUSED, REFUSED_DAMAGED, WRONG_ADDRESS, NOT_COLLECTED); statuses **ATTEMPT_FAILED, HELD_AT_OFFICE, RETURNING, RETURNED**; the **held-at place** on the fulfillment; timers stored as data (not code constants); the **reverse direction** allowed on a leg (A → B and B → A) | now |
| **1 — F1b auto-confirm** | the 3 / 7-day window + reminders (reuses the digital auto-confirm job) | first, after launch — unblocks seller and shipper money |
| **2 — attempts + office hold (F1, F2 unreachable)** | attempt button for riders, reschedule for buyers, 7-day hold, collect / redeliver | next |
| **3 — returns (F1 step 4, F2 refused, F3)** | reverse legs back to the seller, refunds minus costs, who-pays rules | with or after disputes |
| **4 — disputes (F4)** | the full draft in §15 | later |

## 16. To do after launch

- ⭐ Recommended uses reliability (delivery history).
- Real ETAs from recorded delivery times.
- Licence expiry / document renewal (KYC re-check).
- Coverage exclusions ("all Dar except Kigamboni").
- Shop front photo.
- **Paid packing by shippers** ("we pack for you") for sellers without boxes.
- Seller may drop a parcel at a door shipper's office (instead of being collected at the shop).
- **Seller drops at stations**: shop setting "I can drop parcels at stations for free" + the districts they will go to
  (area picker; the shop's own district pre-ticked) → a station route without the door leg.
- Shipper team members (dispatcher / rider roles, job assignment, rider shown to the buyer).
- Account deletion removes addresses.
- Location step 4 records recent pins at checkout-session creation — change to **after payment** when building (§4.4).
- New dedicated shipping notifications (after the shipping service).
- Staging / production: PostGIS, ward file (at deployment).

---

## 17. Worked scenarios

Illustrative numbers. Each scenario uses only rules decided above.

### A — Pickup, collected
Buyer in Sinza buys shoes from a shop in Ubungo, chooses **Pickup**.

| Step | Who | What happens | Money |
|---|---|---|---|
| 1 | Buyer | pays 60,000 | items escrow 60,000 HELD |
| 2 | Seller | accepts, then taps **Ready** | — |
| 3 | Buyer | told "ready at Ubungo, Gate B" (directions, phone); picks **"I'll come Thu 15:00"** (optional) | — |
| 3a | Seller | sees "Buyer coming Thu 15:00"; buyer reminded Thursday morning | — |
| 4 | Buyer + seller | buyer shows code **4821**, seller enters it → *collected* | — |
| 5 | Buyer | taps **"✓ It's what I ordered"** | items released to seller (minus fee) |

If the buyer never comes: reminders, then after **14 days** auto-cancel → 60,000 refunded, stock back.

| Instead | Who | What happens | Money |
|---|---|---|---|
| Day 2, buyer phones: "I can't come, cancel" | Seller | **Cancel** → pop-up "I have already informed the buyer" → confirm | 60,000 refunded, stock back |
| Before Ready, seller finds the shoes damaged, calls the buyer | Seller | same pop-up → confirm; fixes stock by hand | 60,000 refunded |

### B — Door-to-door, one shipper
Phone 400,000, shop in Kariakoo → buyer's Home in Mikocheni (~9 km).

| Step | Who | What happens | Money |
|---|---|---|---|
| 1 | Buyer | sheet shows quotation: 💰 Dar Cargo 7,000 · 1–2 h | — |
| 2 | Buyer | checkout → **agreement**; pays 407,000 | items 400,000 + leg 7,000 HELD |
| 3 | Seller | accepts (Dar Cargo gets a heads-up), packs, taps **Ready** | — |
| 4 | Dar Cargo | accepts the offer → receives pickup code | — |
| 5 | Seller | verifies the rider's pickup code, hands over → *picked up* | — |
| 6 | Buyer | told "on the way", receives delivery code **0937** | — |
| 7 | Rider | buyer tells the code, rider submits it → *delivered* | — |
| 8 | Buyer | opens, taps **"✓ It's what I ordered"** | items → seller, 7,000 → Dar Cargo (one transaction) |

### C — Multi-shipper, door in another region
Laptop 1,500,000, shop at Mlimani City (Dar) → buyer's door in Mbeya.

| Leg | Shipper | From → to | Cost | ETA |
|---|---|---|---|---|
| 1 | 🚪 Kibuti Express | shop → Shabiby office, Magufuli Terminal | 3,500 | 2–3 h |
| 2 | 🚌 Shabiby | Magufuli → Shabiby office, Mbeya Stand | 21,000 | 1 day |
| 3 | 🚪 Mbeya Riders | Mbeya Stand → buyer's door | 2,500 | 1–2 h |

| Step | What happens |
|---|---|
| Quotation | buyer edits: none — or "collect at Mbeya Stand" removes leg 3 (−2,500) |
| Ready | **all three legs get the offer at once**; parcel stays at the shop until **all** accept |
| Handovers | shop → Kibuti (pickup code) · Kibuti → Shabiby ("handed over" / "received" + code) · Shabiby → Mbeya Riders (same) · Mbeya Riders → buyer (delivery code) |
| Release | buyer taps "✓ It's what I ordered" → items + all 3 legs released together |

### D — Reassignment, nearby swap (automatic)
In C, Shabiby **rejects** ("full today").

| Step | Result |
|---|---|
| Search step 1 (1 km) | Abood: offices 300 m (Magufuli) and 500 m (Mbeya Stand) from Shabiby's · 20,000 · 1 day |
| Compare | cheaper, not slower → **automatic** |
| Money | leg-2 escrow re-pointed to Abood; **1,000 refunded** to the buyer |
| Others | Kibuti and Mbeya Riders keep their leg and price; get the new exact office ("Abood office, gate 5"), no re-confirm |
| Buyer | told "New courier for Dar → Mbeya: Abood, 1,000 refunded" |

### E — Reassignment, same district (re-confirm)
In C, no other bus company within 1 km at Magufuli; one leaves from **Ubungo Terminal** (same district).

| Step | Result |
|---|---|
| Search step 2 (district) | Kilimanjaro Express from Ubungo · 21,500 |
| Neighbours | Kibuti re-routed shop → Ubungo Terminal, re-costed 3,000 (shorter) |
| Compare | affected total 24,500 vs agreed 24,500 → same, not slower → **automatic** |
| Re-confirm | Kibuti had already accepted and its drop point moved > 1 km → **re-confirm** "drop at Ubungo Terminal instead, OK?" |
| If Kibuti says no | its leg is reassigned in turn (counts toward the **3 per order** guard) |

### F — Chain fails → buyer chooses
In C, nobody in the district can do leg 2 at the agreed price; best is 30,000 (+9,000).

| Buyer chooses | Result |
|---|---|
| Pay the difference | +9,000 from wallet into the leg-2 escrow; new shipper gets the offer |
| End the chain earlier | e.g. collect in Dar — later legs refunded |
| Pickup | all leg escrows refunded (27,000) |
| New delivery proposal | new quotation → new agreement |
| **Cancel the whole order** | full refund; stock back (allowed because the chain failed) |

### G — Switching
| Case | Allowed? | Money |
|---|---|---|
| Ordered **Pickup**, seller already Ready, buyer taps "Deliver it instead" | ✅ before collecting | fresh quotation; pays delivery cost; offer goes to the shipper **at once** |
| Ordered **Delivery**, no shipper accepted yet, buyer switches to pickup | ✅ | legs refunded |
| Ordered **Delivery**, shipper accepted, not heading to the shop yet | ✅ free | legs refunded |
| Ordered **Delivery**, shipper heading to the shop | ✅ with a fee | leg refunded minus the wasted-trip fee |
| Ordered **Delivery**, parcel picked up | ❌ | only a destination change (scenario O) |

### H — Cancelling
| When | Allowed? | Money |
|---|---|---|
| Before the seller accepts | ✅ whole order | full refund |
| Seller accepted, no shipper accepted | ✅ shipping only (pickup / new proposal) | legs refunded or adjusted |
| Shipper accepted, not heading to the shop | ✅ shipping only, free | legs refunded |
| Shipper heading to the shop | ✅ shipping only | Mbezi Boda leg 3,500 → buyer gets 1,750 back, Mbezi Boda gets 1,750 |
| Parcel picked up | ❌ shipping | only a destination change |
| Chain failed | ✅ shipping or the whole order | as chosen |

### I — Free shipping, stacked
Shoes 60,000, shop in Kinondoni → buyer in Kinondoni (~4 km). Shop: free delivery in Kinondoni, pays up to 4,000.
NexGate weekend campaign in Dar: pays up to 3,000.

| Option | Cost | Seller pays | NexGate pays | Buyer pays |
|---|---|---|---|---|
| 💰 Mbezi Boda (cheapest qualifying) | 3,500 | 3,500 | 0 | **0 (Free)** |
| ⚡ Express Riders (faster) | 5,000 | 3,500 | 0 | **1,500** (the difference — the discount is what the cheapest qualifying option costs) |
| a 9 km trip (other buyer) | 7,500 | 4,000 | 3,000 | **500** |

Shipper always paid the full cost: leg escrow = buyer share + NexGate share (campaign account, at payment) +
seller share (taken from the items escrow at release).

### J — Group purchase
| Case | Result |
|---|---|
| Member pays (delivery 3,500) | items + leg escrows HELD; price frozen even if the group fills days later |
| Group fails / member withdraws | items + 3,500 refunded |
| Payment moved to another group | fulfillment moves with it |
| Group fills, shipper rejects at Ready | normal reassignment (D–F) |

### K — Installments
| Mode | Delivery |
|---|---|
| Immediate (ships after down payment) | full delivery cost paid **with the down payment** |
| Layaway (ships after the last instalment) | pickup / delivery chosen and paid **after the last instalment** |
| Agreement cancelled before shipping | delivery cost refunded |

### L — Estimation on a card
Buyer's header address: Home, Kijitonyama. Product from a shop in Ubungo.

| Shippers covering | Card shows |
|---|---|
| Mbezi Boda 3,500 · 40–60 min, Dar Cargo 7,000 · 1–2 h | 🚚 3,500 – 7,000 · Kijitonyama |
| one shipper, 5,000 | 🚚 5,000 |
| free option + faster ones up to 2,000 | 🚚 Free – 2,000 |
| none | Pickup only at Ubungo |

### N — Bulky mismatch
Seller lists a 65" TV (value 1,800,000) **without** "bulky". Buyer chose 💰 Mbezi Boda 3,500.

| Step | What happens | Money |
|---|---|---|
| Rider arrives at the shop | taps "Can't carry — too big" + photo (before pickup) | — |
| Reassignment (bulky only) | Dar Cargo (car) 7,000 + bulky extra 15,000 = 22,000 | buyer pays nothing more |
| Seller pays | difference 22,000 − 3,500 = **18,500** + Mbezi Boda's wasted trip **1,750** (50%) | taken from the items escrow at release |
| Buyer told | "Too big for a boda — a car is coming, no extra cost to you" | — |
| Product flag | 1st mismatch → seller asked to tick bulky; if a 2nd shipper reports it on a later order → bulky set automatically | — |
| Mbezi Boda | reliability **not** affected | gets 1,750 |

### M — A shipper's day in the app (Mbezi Boda, owner only)

| Time | Screen | What the owner sees / does | Data visible |
|---|---|---|---|
| 08:05 | Upcoming | new card "Sinza → Mbezi · small · 3,500 — Waiting for the seller to accept" (buyer just paid) | areas only, size, value band, cost |
| 08:40 | Upcoming | chip changes to "Seller is packing" + heads-up push "order coming from Sinza in ~15 min" | areas only |
| 09:00 | Offers | seller tapped Ready → offer with a 30-min countdown | areas only, map of the two areas |
| 09:04 | Offers | taps **Accept** → job moves to Active; pickup code **4821** shown | **full details now**: shop Gate B + phone, buyer pin, directions "blue gate", receiver Mama Json +255712… |
| 09:20 | Active job | at the shop: tells code 4821, seller verifies → taps **Picked up** | — |
| 09:45 | Active job | at the door: buyer says **0937** → enters it → **Delivered** | — |
| 11:30 | Earnings | job shows 3,500 **held** (waiting for the buyer's "✓ It's what I ordered") | — |
| 12:10 | Earnings | buyer confirmed → 3,500 **released** to the wallet (commission 0%) | — |
| 14:00 | Offers | another offer, but busy → **Reject** "too busy" → reliability notes it; order is reassigned | — |

### O — Destination change (agreement change)
Buyer ordered shoes to home in Sinza with **Mbezi Boda 3,500**; at work in Mlimani City they change the destination.

| Case | Mbezi Boda answers | Result |
|---|---|---|
| Before pickup, new cost 4,500 | **Accept at the new cost** | buyer pays +1,000 into the leg escrow |
| Before pickup, new cost 4,500 | **Accept at the old price** | nothing to pay |
| Before pickup | **Decline** | leg reassigned: Kibuti Express 4,000 is dearer → buyer decides (pay +500 / keep Sinza / pickup) |
| Before pickup | no answer in 30 min | Sinza stays; the buyer is told |
| After pickup, new cost 2,500 | **Accept** | buyer gets 1,000 back |
| After pickup | **Decline** | Sinza stays — the rider keeps the parcel and delivers as agreed |
| Delivered | — | not allowed |

### P — Cancelling the shipping (signed shipper)
Same order, Mbezi Boda signed (accepted) at 09:04.

| When the buyer cancels the shipping | Fee | Money |
|---|---|---|
| 09:10 — accepted, not set out | none (like Bolt) | 3,500 refunded; Mbezi Boda released, reliability unaffected |
| 09:15 — Mbezi Boda tapped **Heading to the shop** | wasted trip 50% | 1,750 refunded to the buyer, 1,750 to Mbezi Boda |
| 09:20 — picked up | — | not allowed; destination change only |