Today, when something goes wrong mid-swap, the station manager phones HQ, the support specialist opens three external tools to investigate, and money moves through informal channels. There’s no audit trail and no SLA. The two surfaces in this PRD close that gap:
Swap app gives the manager inline issue capture at the exact step where the issue manifests — no separate “report support ticket” mode. The manager stays in flow, parks the blocked swap, serves the next rider, and gets a push when support resolves it.
Atlas gives the support specialist a single-page resolution surface with the case, the parties (rider · manager · the other manager if relevant · evidence), and the action buttons that actually move money or batteries — without leaving the page. Every action is audit-logged automatically.
The 17 May overdraft mis-credit incident — where an overdraft was issued to a rider whose case had no wallet need — is the proximate driver for the new server-side eligibility guardrail (section 8.2).
Run swaps fast. When something blocks the swap, raise the issue in one tap at the step where it surfaced, with all context auto-captured. Park, serve next rider, resume on push.
Swap support specialist (“Tunde”)
Atlas (desktop, 1440×900)
Yes — existing user, new experience built for them
Triage and resolve in-flight issues raised from the field. One page per case. Money / battery / station mutations happen inside the case page. SLA-bound.
Swap PM (“Ridwan”)
Atlas — PM view
Yes
Read-only operational overview: hot stations, hot riders, anomaly flags, team scoreboard, audit feed. No mutation surface.
Out-of-scope personas (this round): rider, charge-hub operator, finance reconciler.
14 operational scenarios (catalog in §5). Cover wallet, payment, battery-location, primary-station, and outstanding-overdraft cases.
Swap app: inline alerts at the manifesting step · parked-swap queue · push notification on resolution · in-app support log (read-only history) · catch-all support sheet for unlisted issues.
Atlas: support queue dashboard · issue-detail with embedded resolution panel · audit trail (per-specialist + team feed) · PM dashboard (KPIs, distribution, anomalies).
VCU manual-unlock fallback (scenarios 12 / 13 / 14). The bluetooth pairing that the manager runs at every swap stays in the swap app. The internet / manual-code resolution path is being redesigned to live entirely in-app with no human in the loop; until that design lands, the backoffice carries no VCU resolution surface.
The single label for any escalation from swap-app to backoffice. Internally support may route to engineering, but the manager-facing label is always this
“Send to Ops”, “Send to engineering”, “Log to backoffice”, “Send to support team”
3 decisions: allow + ₦500 fee · allow + waive fee · deny (rider returns to primary) · pattern flag if ≥3 temp this week
Med
2
Primary station change
Quick request from support page · 2-step form · pending → approved push
Approve/deny · both stations + rider notified · effective immediately or scheduled
Low
Wallet & payments — scenarios 4, 6, 7, 8, 8a share one entry point: the consolidated “How is the rider paying?” alert on the rider profile (5 actions). The five backend paths remain distinct.
3
Wallet not loading
Rider profile · retry · park while support rebuilds
Force rebuild from ledger · push manager when ready
Engineering follow-up · safe retry (idempotent, no double-charge)
Med
6
Funding not reflecting
“Log rider’s receipt” · single OR multiple payments · per-payment receipt photo (camera OR gallery) · virtual-account + source-bank pickers
Live settlement-feed tail · gated overdraft / wait / deny
High
7
Station wallet → rider transfer failed
“Pay from station wallet” then transfer fails · retry · escalate
Confirm engineering fix landed · retry transfer on station’s behalf
Med
8
Funds with another station manager
“Funds at another station” · photo evidence + station/manager picker
Click-to-call both managers · move ₦12,000 from station A’s wallet → rider wallet · resolve
High
8a
Funding account number invalid(NEW)
“Funding account invalid” · capture provider · account number shown · rider’s bank error
Support escalates internally to engineering to compare provider-generated VA vs what rider sees
Med
Battery — every battery scenario (9/10/11) traces back to the same root cause: a field-support staff did a swap without using the app, leaving both batteries with the wrong digital position. Scenario 16 is a separate problem — a typo/worn printed serial that returns no inventory data when scanned. Scenarios 9/10/11 resolve with a single-click record mutation in Atlas (no relabel). Only 16 keeps the print-and-stick mechanic.
Click-to-call on every contact card. Calls happen externally (no in-app VoIP); we copy the number to clipboard and write an audit entry Number copied.
Overdraft eligibility gate (§8.2) inside the funding-not-reflecting resolution card: a static eligibility line + mandatory confirmation checkbox; “Issue overdraft” button stays disabled until checked. Server-side check mirrors the UI.
Audit trail is append-only and immutable. Corrections are new entries that reference the original.
No park feature in issue detail. The manager parks on the swap-app side; the Atlas specialist either resolves or keeps the case open with a state=in-progress.
REST + JSON. Auth = existing Atlas session for support; existing swap-app JWT for managers. All money-moving and inventory-mutating endpoints accept a client-generated Idempotency-Key header (UUID v4 from the client); the server stores request_hash → response for 24h and returns the cached response on key collision.
The POST /overdraft endpoint MUST reject the request unless ALL of:
linked_case_id is present.
The referenced case.case_type is in the wallet group: funding-not-reflecting · funds-at-another-station · station-wallet-payment-failed · daily-fee-deduction-failed · funding-account-invalid.
The case is in state ∈ {in-progress, pending-external}.
The acting specialist has role=support and is assigned to (or has claim-permission on) the case.
The rider currently has swap_eligible=true (no other outstanding overdraft) OR the request explicitly carries replaces_previous_overdraft=true with the prior case ID.
If any check fails → 403 FORBIDDEN with code=OVERDRAFT_NOT_ELIGIBLE and a details object naming the failed precondition. The Atlas UI mirrors checks 1–4 with a mandatory confirmation checkbox; engineering enforces server-side.
This closes the loophole behind the 17 May incident where an overdraft was credited to a rider whose case had no wallet need.
Write wallet_ledger row with entry_type=overdraft_credit, linked_case_id.
Set riders.swap_eligible=false, riders.swap_eligible_blocking_case_id=<case_id>.
Emit wallet.overdraft audit event.
The flag clears when EITHER:
An inbound provider_funding_inbound matches the rider and is sufficient to settle the open overdraft (auto-reconcile job emits overdraft_reconcile ledger entry + flips flag); OR
A specialist explicitly writes off via POST /v1/cases/:id/write-off (manager has no override).
Swap-app reads riders.swap_eligible on every scan; when false, surfaces the outstanding-overdraft hard block alert (scenario 15) instead of the normal rider profile. Single CTA Open case ISS-XXXX deep-links to the originating case.
POST /v1/wallets/station/:station_id/pay-rider-subscription
Station wallet pays the rider’s daily fee directly (scenario 4, station-pays path)
Body: { rider_id, amount_kobo, linked_swap_id }. Idempotent. Single-step transfer that debits station wallet and marks the swap’s daily-fee as paid. Not the 2-step credit-rider-then-debit model.
POST /v1/wallets/station/:station_id/transfer-to-rider
Used by scenario 7 after a failed station-wallet payment that engineering has since fixed; support retries from Atlas
One endpoint per provider (Opay / Paystack / Monnify). Signature-verified. Writes a provider_funding_inbound row · attempts auto-match to a rider by VA + sender phone
GET /v1/cases/:id/settlement-tail?since=…
Live tail for the funding-not-reflecting case page
Scenario 16: support pushes the closest-match serial back to the manager
Body: { corrected_serial }. Once issued, the next scan from the manager is validated against this; mismatches are written to audit_events.action_type='battery.relabel-mismatch'
All four mutate batteries, write audit_events with full before/after delta_json, and trigger a push to the originating manager so the parked swap can resume.
Body: { case_type, captured_context_json, evidence_uris[], linked_swap_id, raised_by_manager_id, raised_at_station_id, subject_* }. Idempotent on a client-generated key (so a flaky network doesn’t double-raise). Computes severity + sla_target_at from case_type. Auto-assigns by round-robin within severity weight.
GET /v1/cases?filter=…
Atlas queue dashboard
GET /v1/cases/:id
Atlas issue detail
POST /v1/cases/:id/claim
Specialist claims an unassigned case
POST /v1/cases/:id/resolve
Generic resolve — used when no specialised mutation endpoint applies (e.g. temp-swap decisions, primary-change decisions, wait/deny on funding)
Body: { outcome, internal_note, decision_payload }. The outcome enum is one of the §5 scenario-decision options (e.g. temp-swap.allow-with-fee, temp-swap.allow-waive-fee, temp-swap.deny, funding.wait, funding.deny)
pending-external covers cases waiting on an upstream system (engineering fix, finance reconciliation, provider settlement). Auto-transitions back to in-progress when the external signal arrives.
escalated is a hand-off (e.g. funds-other-station that needs the other station’s manager to come on call). Same actor returns to it; it does not exit the support team’s queue.
Every state transition writes an audit_events row.
The swaps.parked_waiting_on enum carries the resume target (opay-funding, battery-release, etc) so the swap-app knows which step to land on when the manager taps the push notification.
Every money-moving + inventory-mutating endpoint accepts Idempotency-Key. Server stores {key → response, request_hash} for 24h. Same key + same body returns the cached response (200/2xx). Same key + different body returns 409 CONFLICT.
One release. Atlas and swap-app deploy together; both surfaces depend on the case service + wallet eligibility flag. No partial rollout possible without breaking the parked-swap resume loop.
Pre-launch:
Backfill riders.swap_eligible=true for every rider (no historical overdrafts in scope).
Backfill stations.wallet_id to point at the existing station-wallet records (already exist in production under the legacy “float” naming).
Run case-service migrations on an empty cases table (no historical case data to import).
Post-launch:
One-week heads-up to the 8 station managers via WhatsApp group with the new in-app flow demoed.
Two-week soft launch with support team on-call extended to 12h/day before scaling SLA enforcement to 24/7.
Station wallet top-up policy. Today stations self-fund their wallet (often the on-duty manager fronts the cash) — do we centralise that in Atlas v2 this round, or defer? Owner: Ridwan + Finance.
Overdraft accounting. Where do unmatched overdrafts live in our books while waiting for bank settlement? Owner: Finance.
Overdraft ceiling. When the bank is slow on a funding-not-reflecting case, support overdrafts up to ₦X per rider per day. What’s X? Owner: Finance.
Primary station change cooldown. Prevent the same rider from changing primary station more than once a month? Prevents wallet-arbitrage gaming. Owner: Ridwan.
SLA targets. §11.1 numbers are proposed — need leadership sign-off.
UAT is a manual checklist exercised by the swap PM + one support specialist + one station manager on a staging environment with seeded data. Test cases below are written so a tester unfamiliar with the codebase can execute them.
Open every prototype screen · grep the UI text · no occurrence of “permanent station”, “manager wallet”, “manager float”, “station float”, “Send to Ops”, “Send to engineering”, “Log to backoffice”
C-6
No park button in Atlas issue detail
Open any case · the right-column resolution panel shows only the primary action button(s) — no “Park”, no “Keep open · ping me”, no “Save note · keep open”
Manager scans a rider whose primary station is NOT this station
Inline Temporary swap alert visible · primary station name shown
S1-2
Tap “Request authorization” → fill reason → “Send to support”
Case created in Atlas with case_type=temp-swap · captured-context includes rider, plate, stations, reason · parked card on dashboard
S1-3
Atlas: support opens case · resolution panel exposes three distinct buttons: “Allow swap · charge ₦500 fee”, “Allow swap · waive fee”, “Deny · rider returns to primary station”
All three present · no other action options
S1-4
Support clicks “Allow · waive fee”
Push lands on manager phone · status card on swap-app updates to approved · audit shows case.resolved with outcome=temp-swap.allow-waive-fee · daily fee not increased by ₦500
S1-5
Repeat with “Deny”
Manager sees denied status · daily fee not drawn · swap cancellable
Manager scans a new battery serial that’s still attached to KOZ-3829-LA per inventory
Inline alert “Release-and-reassign” visible · “Send to support” button
S9-2
Atlas: one click “Release & reassign · resolve”
batteries.current_keke_id flips to null · current_station_id set to calling station · paired audit entries · push to manager · parked swap can resume
S10-1
Manager scans a battery recorded at a different station
Inline alert “Station claim” visible
S10-2
Atlas: resolve
batteries.current_station_id flips to calling station · audit entry · push
S11-1
Manager pairs VCU on a keke with current_battery_id=null
VCU Current battery in keke card shows empty state · alert “Old-battery attribution” visible
S11-2
Atlas: confirm candidate serial with manager (phone copy) · resolve
kekes.current_battery_id set · audit entry · swap-app refreshes the VCU card so manager can advance
S16-1
Manager scans a serial returning no inventory
Inline alert “Serial mismatch” visible
S16-2
Atlas: pushes back the closest-match serial
cases.captured_context_json.corrected_serial set · push to manager with a clipboard-copy chip · manager prints, sticks, rescans · next scan validates against corrected serial · mismatch on rescan writes battery.relabel-mismatch audit
Pre-condition: rider has an open overdraft from S6 yesterday · swap_eligible=false
S15-2
Manager scans this rider
Rider profile shows the hard-block alert “Outstanding overdraft · cannot swap” with the unsettled amount · Send to support is the ONLY action · no “Pay from station wallet” or other overrides
S15-3
Inject an inbound matching the rider
Auto-reconcile fires · ledger row written · swap_eligible flips to true · hard-block alert clears on next refresh