# Baskit Picker — Backend Ops Gap Analysis

**For:** Baskit backend ops — https://baskitops.appnotify.co.za
**From:** Flutter picker app build (this repo)
**Base path:** `/api/v1` · **Auth:** picker-scoped JWT (`Authorization: Bearer …`)
**Reads alongside:** the *Baskit Picker App* scope (§05 APIs, §06 Backend deltas, §07 Data model) and the DC prototype in `docs/UI/Baskit Picker App.dc.html`.

This document lists **what the built Flutter app needs from the backend beyond the 13 endpoints already in the scope** — extra screens the scope under-counts, extra endpoints, request/response shapes that need pinning down, and cross-cutting concerns (realtime, offline, permissions). Everything the app currently runs against is defined in `lib/data/picker_api.dart`; today it is served by `MockPickerApi`. Swap in `HttpPickerApi` once the endpoints below exist.

Priority key: **P0** = blocks a Must (PK-01…PK-10) flow · **P1** = blocks a Should · **P2** = production hardening.

---

## 1. Endpoints already in scope (implemented against)

These 13 are wired as the `PickerApi` contract and exercised by the app. No change requested other than the body/response refinements in §4.

| # | Method | Path | Used by |
|---|--------|------|---------|
| 1 | POST | `/auth/login` | Login |
| 2 | PATCH | `/picker/status` | Duty toggle (+ location) |
| 3 | GET | `/picker/jobs` | Job feed (available / active / today) |
| 4 | GET | `/picker/jobs/{id}` | Job detail |
| 5 | POST | `/picker/jobs/{id}:accept` | Claim job |
| 6 | POST | `/picker/jobs/{id}:decline` | Pass job |
| 7 | POST | `/picker/jobs/{id}/items/{itemId}:pick` | Mark picked |
| 8 | POST | `/picker/jobs/{id}/items/{itemId}:substitute` | Record swap |
| 9 | POST | `/picker/jobs/{id}/items/{itemId}:refund` | Refund + skip |
| 10 | POST | `/picker/jobs/{id}:spend` | Record card spend + receipt |
| 11 | POST | `/picker/jobs/{id}:handoff` | Release against code |
| 12 | GET | `/picker/earnings` | Earnings |
| 13 | POST | `/devices` | Register push token |

---

## 2. Extra screens the app has (scope names "six")

The scope §03 states the app "runs on six screens" (feed, detail, active pick, substitute, handoff, earnings). The prototype and this build actually ship **five more surfaces** that the scope needs to acknowledge because each has backend needs:

| Screen / surface | Status in build | Backend dependency | Priority |
|---|---|---|---|
| **Sign in** | Built | `POST /auth/login` (exists). Needs approval state in response — see §3. | P0 |
| **Barcode scanner** (scan-to-pick) | Built | `:pick` must accept a scanned barcode + method — see §4. | P0 |
| **Record card spend** (sheet) | Built | `:spend` + receipt media upload — see §4. | P0 |
| **Profile / "Me"** | Built | No profile endpoint in scope — see §3 (`GET /picker/me`). | P1 |
| **Push banners** (new job / pack-by) | Built (simulated) | Server-push content + a deadline scheduler — see §5. | P1 |

**Screens still missing / stubbed** (present as dead links or not modelled) that production needs:

| Missing screen | Trigger in UI | Needs |
|---|---|---|
| **Forgot / reset PIN** | "Forgot?" link on login (dead) | §3 auth-lifecycle endpoints |
| **Pending-approval state** | PK-01: "only approved pickers see jobs" — what a signed-in but unapproved picker sees | Approval status from login/me |
| **Help & support** | Profile row "Help & support ›" (dead) | Static content or support ticket endpoint (out of core scope) |
| **Driver handoff detail / call driver** | Handoff shows driver + call button, live ETA | §3 driver + handoff GET; open item §6 |
| **Permission / error / empty states** | Location denied, camera denied, session expired, no jobs, network error | Standardised error contract + `401→refresh` |

---

## 3. Extra endpoints needed

| # | Method | Path | Why the app needs it | Priority |
|---|--------|------|----------------------|----------|
| A1 | POST | `/auth/refresh` | Picker JWTs expire; the app must refresh silently instead of dumping the picker mid-pick. | **P0** |
| A2 | POST | `/auth/logout` | "Sign out" on Profile should revoke the token / deregister the device. | P1 |
| A3 | POST | `/auth/pin/reset-request` + `/auth/pin/reset` | Back the "Forgot?" link. | P1 |
| A4 | GET | `/picker/me` | Profile screen (name, area, rating, picks, since, card ••••, transport, notif prefs) currently only comes from the login payload; a persistent app must re-fetch it. | P1 |
| A5 | PATCH | `/picker/me` | Edit transport mode / notification preferences shown on Profile. | P2 |
| A6 | POST | `/picker/jobs/{id}/items/{itemId}:ask-substitute` | The Substitute sheet's **"Ask the customer"** flow (PK-08) *sends a proposed swap to the customer and awaits a reply*. Scope's `:substitute` only **records a decided** swap — it does not model the ask/await. | **P0** |
| A7 | GET | `/picker/jobs/{id}/items/{itemId}/substitute-request` | Poll the customer's reply to A6 **if** no realtime channel exists (see §5). | P1 |
| A8 | POST | `/picker/jobs/{id}/receipts` (multipart) → `{ receiptRef }` | Upload the till-slip photo (PK-06) and the scanner's "snap a photo of the label". `:spend` then references `receiptRef`. Scope lists `:spend` but never defines the media mechanism. | **P0** |
| A9 | GET | `/picker/jobs/{id}/handoff` → `{ code, driver, eta, driverLocation }` | Handoff screen shows the generated code **and** the assigned driver, ETA and distance. Scope generates codes server-side but exposes no way to read the driver info. | **P0** |
| A10 | POST | `/picker/jobs/{id}/driver:call` (masked) | "Call driver" button on Handoff. Alternatively surface a masked number in A9. | P2 |
| A11 | DELETE | `/devices/{token}` | Deregister push token on sign-out (pairs with A2). | P2 |

---

## 4. Refinements to existing endpoints (request/response shapes)

These are **not new endpoints** — they are body/response fields the app relies on that the scope leaves unspecified.

- **`:pick` (7)** — must accept `{ method: "scan" | "photo" | "manual", barcode?, qty }`. The scanner validates the scanned EAN against the expected product; the backend should confirm the match (or return a mismatch so the app can warn). Manual/photo picks record `method` for audit.
- **`:accept` (5)** — must define **`409 Conflict`** when the job is already claimed (claim-lock race, PK-04). The app needs this to show "job already taken" and refresh the feed rather than silently proceeding.
- **`:spend` (10)** — body `{ actualSpendCents, receiptRef }` (integer cents; `receiptRef` from A8). Response should echo the reconciliation status so the app can confirm it reached reconciliation (§06 tie-in).
- **`:handoff` (11)** — must carry the **bag grouping** the app computes (cold / fresh / bakery) and the verified `handoffCode`: `{ code, bags: [{ label, tag, items:[itemId] }] }`. Scope §06 says "record bag grouping and the driver" but the payload is undefined.
- **`:substitute` / `:refund` (8, 9)** — body should include the resolution source `{ decision: "auto" | "customer", customerPref: bool }` to populate `substitutions.customer_pref` / `status`.
- **`/picker/jobs` & `/picker/jobs/{id}`** — must include **store geolocation** (lat/lng) and per-item **aisle** so the feed can rank by distance (uses picker location from `:status`) and the active pick can group by aisle. The prototype hard-codes these.
- **All mutations (`:pick`, `:substitute`, `:refund`, `:spend`, `:handoff`)** — accept an **`Idempotency-Key`** header. Required for offline replay (PK-13); see §5.
- **`/picker/earnings`** — optional `?range=day|week` and money as integer cents. History rows already match the UI list.

---

## 5. Cross-cutting concerns

1. **Realtime vs polling (P0 decision).** Three flows are inherently push-driven:
   - Feed churn — a job another picker claims must leave this picker's feed (PK-04).
   - "Ask the customer" reply (A6/A7).
   - New-job and pack-by push (PK-12).
   Recommend an authenticated **WebSocket/SSE channel** (`/picker/stream`) carrying `job.claimed`, `substitute.replied`, `job.new`, `job.deadline` events. Without it the app must poll `GET /picker/jobs` and A7, which is wasteful and laggy. **Please decide.**

2. **Offline sync (PK-13, P1).** The active pick must work through brief dropouts and replay on reconnect. Requires: idempotency keys on all mutations (§4), server tolerance of out-of-order event arrival, and a per-item `updatedAt`/version so the client can reconcile. Optionally a batch replay endpoint `POST /picker/jobs/{id}/events`.

3. **Push notification content + scheduler (PK-12, P1).** `POST /devices` only registers the token. The backend still needs to **send** new-nearby-job pushes (geofenced on picker location) and a **pack-by deadline scheduler** that fires the "12 min left" reminder.

4. **Permissions & error contract (P1).** The app needs a consistent error envelope (`{ code, message }`) and a documented `401` → refresh (A1) → retry path. Camera (scanner) and location (feed ranking) permission denials are client-side but the API must degrade gracefully when location is absent.

---

## 6. Open items from scope §10 that block the app

These scope-level decisions change what the app builds — flagging as blockers:

1. **Picker model — employed (assignment) vs gig (open claim).** This build implements the **gig / claimable feed** (matches the designed screens). If pickers are employed, the feed becomes an assignment inbox and `:accept`/`:decline` semantics change. **Confirm before wiring dispatch.**
2. **Batch & bonus rules.** When same-store jobs batch and how `+R12 batch bonus` is computed — the app displays it but the rule is server-owned.
3. **Prepaid-card issuer.** How cards are provisioned/funded and how the transaction feed is retrieved — affects whether `:spend` reconciliation is manual (receipt only) or fed by a card-txn webhook.
4. **Handoff with the courier.** Whether the **driver** enters the code in a partner app or the picker verifies it in this app. Determines whether A9 is read-only (display code) or the app needs a `:verify-handoff` action.

---

## 7. Screen → endpoint map (as built)

| Screen | Endpoints (scope + proposed) |
|---|---|
| Login | `POST /auth/login`; A1 refresh; A3 reset |
| Job feed | `GET /picker/jobs`; `PATCH /picker/status`; realtime `job.*` |
| Job detail | `GET /picker/jobs/{id}` |
| Active pick | `:accept`, `:pick` (+barcode), `:decline` |
| Barcode scanner | `:pick { method, barcode }`; A8 receipts (label photo) |
| Substitute | `:substitute`, `:refund`, **A6 ask-substitute**, A7 poll |
| Record spend | **A8 receipts**, `:spend` |
| Handoff | **A9 handoff GET**, `:handoff { code, bags }`, A10 call driver |
| Earnings | `GET /picker/earnings` |
| Profile / Me | **A4 me**, A5 update, `PATCH /picker/status`, A2 logout, A11 device delete |
| Push banners | `POST /devices`, server push (PK-12) |

---

## Summary of asks (remaining)

Most **Must** feed/claim/pick mutations are now live on Ops. Remaining hardening:

- **P0 leftovers:** realtime vs polling decision · ask-substitute customer reply UX · receipt multipart from the Flutter spend sheet · formal fee table for pay_cents.
- **P1:** PIN reset email deliverability · offline idempotency · push scheduler · error contract polish.
- **P2:** call-driver masking polish · device delete on sign-out.

## Live verification (2026-07-13)

- Host: `https://baskitops.appnotify.co.za/api/v1`
- `GET /config` → `{ ok, picker: true }`
- `POST /auth/login` with phone + PIN → JWT + profile
- `GET /picker/jobs` returns pending store legs from Ops orders
- `:accept` claim-lock (`409` if taken), `:pick`, earnings all exercised against production

