feat(m4): Driver Matching & Ride Lifecycle#16
Conversation
- Nearest-first cascade matching algorithm (Redis GEOSEARCH + busy filter)
- 5km initial radius, 10km fallback, max 10 cascade attempts
- 30s offer timeout with delayed BullMQ jobs
- Cross-process Socket.io emission via Redis pub/sub
- Ride state machine (REQUESTED→ACCEPTED→STARTED→COMPLETED)
- validateTransition() throws on invalid transitions
- Terminal states (COMPLETED, CANCELLED) block all transitions
- OTP system for pickup verification
- 4-digit numeric OTP, Redis TTL 15min, max 3 attempts
- Auto-cancel on max attempts exceeded
- Full ride-lifecycle worker (ACCEPT, VERIFY_OTP, CANCEL)
- ACCEPT: lock (SETNX), offer update, busy set, OTP gen, notify rider
- VERIFY_OTP: compare OTP, track attempts, auto-transition to STARTED
- CANCEL: cleanup Redis (OTP, lock, busy), expire pending offers, notify rooms
- Full ride-matching worker with cascade dispatch
- Socket events: driver:accept-ride, driver:reject-ride (with SETNX lock)
- REST endpoints: POST /rides/:tripId/accept|reject|verify-otp|driver-cancel
Enhanced PATCH /rides/cancel with { tripId } body
- Schema migration: RideOffer model + OfferStatus enum + Trip.otp/fare/distance
- Matching constants + OTP Zod schemas in packages/common
- Vitest setup for ride-worker (state machine: 13 tests)
- Updated ride.service tests (11 tests covering accept/reject/OTP/cancel)
Tests: 71 passed (58 api-gateway + 13 ride-worker)
TypeScript: api-gateway OK | ride-worker OK | common OK
Closes #9
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (21)
📝 WalkthroughWalkthroughImplements the complete M4 Driver Matching & Ride Lifecycle milestone, introducing a Redis-based cascade driver matching algorithm, OTP-based pickup verification, ride state machine with enforced transitions, and lifecycle-based processing for ride acceptance, rejection, and cancellation. Includes API routes, socket handlers, database schema changes, and comprehensive worker implementations. Changes
Sequence DiagramssequenceDiagram
participant Rider
participant API Gateway
participant Redis
participant Ride Worker
participant DB as Database
participant Driver
Rider->>API Gateway: POST /rides (create ride request)
API Gateway->>Ride Worker: Publish ride-requests job
Ride Worker->>Redis: GEOSEARCH nearby drivers (5km)
Redis-->>Ride Worker: List of nearby driver IDs
Ride Worker->>DB: Filter out busy drivers
Ride Worker->>DB: Create RideOffer (PENDING, expireAt: now+30s)
Ride Worker->>Redis: Publish ride:offer via Socket.io
Redis-->>Driver: ride:offer (tripId, offerId)
Ride Worker->>Ride Worker: Schedule offer-timeout job (30s delay)
alt Driver Accepts
Driver->>API Gateway: Socket: driver:accept-ride
API Gateway->>Redis: SETNX ride:lock:{tripId}
Redis-->>API Gateway: Lock acquired
API Gateway->>Ride Worker: Publish ride-lifecycle ACCEPT job
Ride Worker->>DB: Update RideOffer to ACCEPTED
Ride Worker->>DB: Update Trip: driverId, status=ACCEPTED
Ride Worker->>Redis: Add driver to drivers:busy set
Ride Worker->>Redis: Store OTP (otp:{tripId}, TTL=15m)
Ride Worker->>DB: Save OTP to Trip record
Ride Worker->>Redis: Publish ride:accepted event
Ride Worker->>Redis: Publish ride:otp event with OTP
Ride Worker-->>Driver: ride:accepted + ride:otp notifications
Ride Worker-->>Rider: ride:accepted + ride:otp notifications
else Offer Timeout (30s)
Ride Worker->>DB: Check if RideOffer still PENDING
Ride Worker->>DB: Mark RideOffer as EXPIRED
Ride Worker->>Redis: Publish ride:offer-expired
Redis-->>Driver: ride:offer-expired
Ride Worker->>Redis: Find next nearest driver (exclude offered drivers)
alt Next Driver Found
Ride Worker->>DB: Create new RideOffer
Ride Worker->>Ride Worker: Schedule new offer-timeout job
else No Drivers Within Radius
Ride Worker->>Redis: Expand radius to 10km, retry
alt Still No Drivers
Ride Worker->>DB: Update Trip status=CANCELLED
Ride Worker->>Redis: Publish ride:cancelled (no drivers available)
Ride Worker-->>Rider: Cancellation notification
end
end
end
sequenceDiagram
participant Driver
participant API Gateway
participant Redis
participant Ride Worker
participant DB as Database
participant Rider
Driver->>API Gateway: POST /rides/{tripId}/verify-otp {otp: "1234"}
API Gateway->>DB: Validate driver assigned to trip
API Gateway->>Ride Worker: Publish ride-lifecycle VERIFY_OTP job
Ride Worker->>Redis: Get OTP from otp:{tripId}
Redis-->>Ride Worker: Stored OTP
Ride Worker->>Redis: Check otp:attempts:{tripId}
alt OTP Match
Ride Worker->>DB: Update Trip status=STARTED
Ride Worker->>Redis: Delete OTP key
Ride Worker->>Redis: Delete attempts counter
Ride Worker->>Redis: Delete ride:lock:{tripId}
Ride Worker->>Redis: Publish ride:started event
Ride Worker-->>Driver: ride:started notification
Ride Worker-->>Rider: ride:started notification
else OTP Mismatch & Attempts < 3
Ride Worker->>Redis: Increment otp:attempts:{tripId}
Ride Worker-->>Driver: OTP error with remaining attempts
else Max Attempts Exceeded
Ride Worker->>DB: Update Trip status=CANCELLED
Ride Worker->>Redis: Publish ride:cancelled (max OTP attempts)
Ride Worker->>Redis: Remove driver from drivers:busy
Ride Worker->>Redis: Clean up OTP and lock keys
Ride Worker-->>Driver: Cancellation notification
Ride Worker-->>Rider: Cancellation notification
end
sequenceDiagram
participant Driver
participant API Gateway
participant Redis
participant Ride Worker
participant DB as Database
participant Rider
Driver->>API Gateway: Socket: driver:reject-ride {tripId, offerId}
API Gateway->>DB: Validate driver & offer ownership
API Gateway->>Ride Worker: Publish ride-matching job (no delay)
Ride Worker->>DB: Mark RideOffer as REJECTED
Ride Worker->>Redis: GEOSEARCH remaining nearby drivers
Ride Worker->>DB: Filter out busy drivers and already-offered drivers
alt Next Driver Available
Ride Worker->>DB: Create new RideOffer
Ride Worker->>Redis: Publish ride:offer to next driver
Ride Worker->>Ride Worker: Schedule offer-timeout job (30s)
else No More Drivers
Ride Worker->>DB: Update Trip status=CANCELLED
Ride Worker->>Redis: Publish ride:cancelled
Ride Worker-->>Rider: Cancellation notification
end
API Gateway-->>Driver: driver:reject-ride:ack
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Comment |
Overview
Implements Issue #9 — nearest-first cascade driver matching, ride state machine, OTP pickup verification, and full ride lifecycle processing.
What Changed
Schema Migration
RideOffermodel +OfferStatusenum (PENDING|ACCEPTED|REJECTED|EXPIRED)otp,fare,distancetoTrippackages/common/prisma/migrations/20260306_add_ride_offers_and_trip_fields/packages/common
constants/matching.ts— MATCHING_RADIUS_KM (5), EXPANDED (10), OFFER_TIMEOUT (30s), MAX_CASCADE (10), Redis key prefixesschemas/otp.schema.ts— Zod schemas for OTP verification + ride cancel with tripIdride-worker — Core Engine
State Machine (
state-machine.ts)Cascade Matching (
matching/cascade.ts)GEOSEARCHRedis GEO for drivers within radius, sorted by distancedrivers:busyRedis setRideOffer→ schedule 30s timeoutride-matching.worker.ts — dispatches cascade/timeout jobs
ride-lifecycle.worker.ts — full implementations:
ride:accepted+ride:otp)api-gateway — API Layer
Socket Events:
driver:accept-ride { tripId, offerId }— SETNX lock → lifecycle ACCEPT jobdriver:reject-ride { tripId, offerId }— mark REJECTED → immediate cascadeREST Endpoints:
/rides/:tripId/accept/rides/:tripId/reject/rides/:tripId/verify-otp/rides/:tripId/driver-cancel/rides/cancel{ tripId }bodyRace Condition Protection:
Cross-Process Notification
ride-worker emits socket events via Redis pub/sub → api-gateway's Redis adapter delivers to clients
Tests
71 tests total (all passing):
Depends On
Blocks
Closes #9
Summary by CodeRabbit